mirror of https://github.com/ansible/ansible.git
remove old dead code
parent
5ba3452b7e
commit
ef594f708c
@ -1,11 +0,0 @@
|
|||||||
This is dead code, it is here for convenience for those testing current devel so as to ascertain if a bug was introduced in the v2 rewrite or was preexisting in the 1.x codebase.
|
|
||||||
Using this code should be equivalent of checking out the v1_last tag, which was devel at a point between 1.9.1 and 1.9.2 releases.
|
|
||||||
The stable-1.9 is the maintenance branch for the 1.9.x code, which might continue to diverge from the v1/ tree as bugs get fixed.
|
|
||||||
|
|
||||||
DO NOT:
|
|
||||||
|
|
||||||
* use this code as reference
|
|
||||||
* make PRs against this code
|
|
||||||
* expect this code to be shipped with the 2.0 version of ansible
|
|
||||||
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
__version__ = '2.0.0'
|
|
||||||
__author__ = 'Michael DeHaan'
|
|
@ -1,61 +0,0 @@
|
|||||||
# (c) 2014, Michael DeHaan <michael.dehaan@gmail.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 collections import MutableMapping
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import constants as C
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
|
|
||||||
class FactCache(MutableMapping):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._plugin = utils.plugins.cache_loader.get(C.CACHE_PLUGIN)
|
|
||||||
if self._plugin is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if key not in self:
|
|
||||||
raise KeyError
|
|
||||||
return self._plugin.get(key)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self._plugin.set(key, value)
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
self._plugin.delete(key)
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
return self._plugin.contains(key)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._plugin.keys())
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._plugin.keys())
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
""" Return a primitive copy of the keys and values from the cache. """
|
|
||||||
return dict([(k, v) for (k, v) in self.iteritems()])
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
return self._plugin.keys()
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
""" Flush the fact cache of all keys. """
|
|
||||||
self._plugin.flush()
|
|
@ -1,41 +0,0 @@
|
|||||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import exceptions
|
|
||||||
|
|
||||||
class BaseCacheModule(object):
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
raise exceptions.NotImplementedError
|
|
||||||
|
|
||||||
def set(self, key, value):
|
|
||||||
raise exceptions.NotImplementedError
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
raise exceptions.NotImplementedError
|
|
||||||
|
|
||||||
def contains(self, key):
|
|
||||||
raise exceptions.NotImplementedError
|
|
||||||
|
|
||||||
def delete(self, key):
|
|
||||||
raise exceptions.NotImplementedError
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
raise exceptions.NotImplementedError
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
raise exceptions.NotImplementedError
|
|
@ -1,143 +0,0 @@
|
|||||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import errno
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible import constants as C
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.cache.base import BaseCacheModule
|
|
||||||
|
|
||||||
class CacheModule(BaseCacheModule):
|
|
||||||
"""
|
|
||||||
A caching module backed by json files.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
self._timeout = float(C.CACHE_PLUGIN_TIMEOUT)
|
|
||||||
self._cache = {}
|
|
||||||
self._cache_dir = C.CACHE_PLUGIN_CONNECTION # expects a dir path
|
|
||||||
if not self._cache_dir:
|
|
||||||
utils.exit("error, fact_caching_connection is not set, cannot use fact cache")
|
|
||||||
|
|
||||||
if not os.path.exists(self._cache_dir):
|
|
||||||
try:
|
|
||||||
os.makedirs(self._cache_dir)
|
|
||||||
except (OSError,IOError), e:
|
|
||||||
utils.warning("error while trying to create cache dir %s : %s" % (self._cache_dir, str(e)))
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
|
|
||||||
if key in self._cache:
|
|
||||||
return self._cache.get(key)
|
|
||||||
|
|
||||||
if self.has_expired(key):
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
|
||||||
try:
|
|
||||||
f = codecs.open(cachefile, 'r', encoding='utf-8')
|
|
||||||
except (OSError,IOError), e:
|
|
||||||
utils.warning("error while trying to read %s : %s" % (cachefile, str(e)))
|
|
||||||
else:
|
|
||||||
value = json.load(f)
|
|
||||||
self._cache[key] = value
|
|
||||||
return value
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def set(self, key, value):
|
|
||||||
|
|
||||||
self._cache[key] = value
|
|
||||||
|
|
||||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
|
||||||
try:
|
|
||||||
f = codecs.open(cachefile, 'w', encoding='utf-8')
|
|
||||||
except (OSError,IOError), e:
|
|
||||||
utils.warning("error while trying to write to %s : %s" % (cachefile, str(e)))
|
|
||||||
else:
|
|
||||||
f.write(utils.jsonify(value))
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def has_expired(self, key):
|
|
||||||
|
|
||||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
|
||||||
try:
|
|
||||||
st = os.stat(cachefile)
|
|
||||||
except (OSError,IOError), e:
|
|
||||||
if e.errno == errno.ENOENT:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
utils.warning("error while trying to stat %s : %s" % (cachefile, str(e)))
|
|
||||||
|
|
||||||
if time.time() - st.st_mtime <= self._timeout:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if key in self._cache:
|
|
||||||
del self._cache[key]
|
|
||||||
return True
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
keys = []
|
|
||||||
for k in os.listdir(self._cache_dir):
|
|
||||||
if not (k.startswith('.') or self.has_expired(k)):
|
|
||||||
keys.append(k)
|
|
||||||
return keys
|
|
||||||
|
|
||||||
def contains(self, key):
|
|
||||||
cachefile = "%s/%s" % (self._cache_dir, key)
|
|
||||||
|
|
||||||
if key in self._cache:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.has_expired(key):
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
st = os.stat(cachefile)
|
|
||||||
return True
|
|
||||||
except (OSError,IOError), e:
|
|
||||||
if e.errno == errno.ENOENT:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
utils.warning("error while trying to stat %s : %s" % (cachefile, str(e)))
|
|
||||||
|
|
||||||
def delete(self, key):
|
|
||||||
del self._cache[key]
|
|
||||||
try:
|
|
||||||
os.remove("%s/%s" % (self._cache_dir, key))
|
|
||||||
except (OSError,IOError), e:
|
|
||||||
pass #TODO: only pass on non existing?
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
self._cache = {}
|
|
||||||
for key in self.keys():
|
|
||||||
self.delete(key)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
ret = dict()
|
|
||||||
for key in self.keys():
|
|
||||||
ret[key] = self.get(key)
|
|
||||||
return ret
|
|
@ -1,191 +0,0 @@
|
|||||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from ansible import constants as C
|
|
||||||
from ansible.cache.base import BaseCacheModule
|
|
||||||
|
|
||||||
try:
|
|
||||||
import memcache
|
|
||||||
except ImportError:
|
|
||||||
print 'python-memcached is required for the memcached fact cache'
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyClientPool(object):
|
|
||||||
"""
|
|
||||||
Memcached connection pooling for thread/fork safety. Inspired by py-redis
|
|
||||||
connection pool.
|
|
||||||
|
|
||||||
Available connections are maintained in a deque and released in a FIFO manner.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.max_connections = kwargs.pop('max_connections', 1024)
|
|
||||||
self.connection_args = args
|
|
||||||
self.connection_kwargs = kwargs
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.pid = os.getpid()
|
|
||||||
self._num_connections = 0
|
|
||||||
self._available_connections = collections.deque(maxlen=self.max_connections)
|
|
||||||
self._locked_connections = set()
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
|
|
||||||
def _check_safe(self):
|
|
||||||
if self.pid != os.getpid():
|
|
||||||
with self._lock:
|
|
||||||
if self.pid == os.getpid():
|
|
||||||
# bail out - another thread already acquired the lock
|
|
||||||
return
|
|
||||||
self.disconnect_all()
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def get_connection(self):
|
|
||||||
self._check_safe()
|
|
||||||
try:
|
|
||||||
connection = self._available_connections.popleft()
|
|
||||||
except IndexError:
|
|
||||||
connection = self.create_connection()
|
|
||||||
self._locked_connections.add(connection)
|
|
||||||
return connection
|
|
||||||
|
|
||||||
def create_connection(self):
|
|
||||||
if self._num_connections >= self.max_connections:
|
|
||||||
raise RuntimeError("Too many memcached connections")
|
|
||||||
self._num_connections += 1
|
|
||||||
return memcache.Client(*self.connection_args, **self.connection_kwargs)
|
|
||||||
|
|
||||||
def release_connection(self, connection):
|
|
||||||
self._check_safe()
|
|
||||||
self._locked_connections.remove(connection)
|
|
||||||
self._available_connections.append(connection)
|
|
||||||
|
|
||||||
def disconnect_all(self):
|
|
||||||
for conn in chain(self._available_connections, self._locked_connections):
|
|
||||||
conn.disconnect_all()
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
def wrapped(*args, **kwargs):
|
|
||||||
return self._proxy_client(name, *args, **kwargs)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
def _proxy_client(self, name, *args, **kwargs):
|
|
||||||
conn = self.get_connection()
|
|
||||||
|
|
||||||
try:
|
|
||||||
return getattr(conn, name)(*args, **kwargs)
|
|
||||||
finally:
|
|
||||||
self.release_connection(conn)
|
|
||||||
|
|
||||||
|
|
||||||
class CacheModuleKeys(collections.MutableSet):
|
|
||||||
"""
|
|
||||||
A set subclass that keeps track of insertion time and persists
|
|
||||||
the set in memcached.
|
|
||||||
"""
|
|
||||||
PREFIX = 'ansible_cache_keys'
|
|
||||||
|
|
||||||
def __init__(self, cache, *args, **kwargs):
|
|
||||||
self._cache = cache
|
|
||||||
self._keyset = dict(*args, **kwargs)
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
return key in self._keyset
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._keyset)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._keyset)
|
|
||||||
|
|
||||||
def add(self, key):
|
|
||||||
self._keyset[key] = time.time()
|
|
||||||
self._cache.set(self.PREFIX, self._keyset)
|
|
||||||
|
|
||||||
def discard(self, key):
|
|
||||||
del self._keyset[key]
|
|
||||||
self._cache.set(self.PREFIX, self._keyset)
|
|
||||||
|
|
||||||
def remove_by_timerange(self, s_min, s_max):
|
|
||||||
for k in self._keyset.keys():
|
|
||||||
t = self._keyset[k]
|
|
||||||
if s_min < t < s_max:
|
|
||||||
del self._keyset[k]
|
|
||||||
self._cache.set(self.PREFIX, self._keyset)
|
|
||||||
|
|
||||||
|
|
||||||
class CacheModule(BaseCacheModule):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if C.CACHE_PLUGIN_CONNECTION:
|
|
||||||
connection = C.CACHE_PLUGIN_CONNECTION.split(',')
|
|
||||||
else:
|
|
||||||
connection = ['127.0.0.1:11211']
|
|
||||||
|
|
||||||
self._timeout = C.CACHE_PLUGIN_TIMEOUT
|
|
||||||
self._prefix = C.CACHE_PLUGIN_PREFIX
|
|
||||||
self._cache = ProxyClientPool(connection, debug=0)
|
|
||||||
self._keys = CacheModuleKeys(self._cache, self._cache.get(CacheModuleKeys.PREFIX) or [])
|
|
||||||
|
|
||||||
def _make_key(self, key):
|
|
||||||
return "{0}{1}".format(self._prefix, key)
|
|
||||||
|
|
||||||
def _expire_keys(self):
|
|
||||||
if self._timeout > 0:
|
|
||||||
expiry_age = time.time() - self._timeout
|
|
||||||
self._keys.remove_by_timerange(0, expiry_age)
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
value = self._cache.get(self._make_key(key))
|
|
||||||
# guard against the key not being removed from the keyset;
|
|
||||||
# this could happen in cases where the timeout value is changed
|
|
||||||
# between invocations
|
|
||||||
if value is None:
|
|
||||||
self.delete(key)
|
|
||||||
raise KeyError
|
|
||||||
return value
|
|
||||||
|
|
||||||
def set(self, key, value):
|
|
||||||
self._cache.set(self._make_key(key), value, time=self._timeout, min_compress_len=1)
|
|
||||||
self._keys.add(key)
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
self._expire_keys()
|
|
||||||
return list(iter(self._keys))
|
|
||||||
|
|
||||||
def contains(self, key):
|
|
||||||
self._expire_keys()
|
|
||||||
return key in self._keys
|
|
||||||
|
|
||||||
def delete(self, key):
|
|
||||||
self._cache.delete(self._make_key(key))
|
|
||||||
self._keys.discard(key)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
for key in self.keys():
|
|
||||||
self.delete(key)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self._keys.copy()
|
|
@ -1,44 +0,0 @@
|
|||||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
|
||||||
#
|
|
||||||
# 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.cache.base import BaseCacheModule
|
|
||||||
|
|
||||||
class CacheModule(BaseCacheModule):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._cache = {}
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
return self._cache.get(key)
|
|
||||||
|
|
||||||
def set(self, key, value):
|
|
||||||
self._cache[key] = value
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
return self._cache.keys()
|
|
||||||
|
|
||||||
def contains(self, key):
|
|
||||||
return key in self._cache
|
|
||||||
|
|
||||||
def delete(self, key):
|
|
||||||
del self._cache[key]
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
self._cache = {}
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self._cache.copy()
|
|
@ -1,107 +0,0 @@
|
|||||||
# (c) 2014, Brian Coca, Josh Drake, et al
|
|
||||||
#
|
|
||||||
# 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 __future__ import absolute_import
|
|
||||||
import collections
|
|
||||||
# FIXME: can we store these as something else before we ship it?
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ansible import constants as C
|
|
||||||
from ansible.utils import jsonify
|
|
||||||
from ansible.cache.base import BaseCacheModule
|
|
||||||
|
|
||||||
try:
|
|
||||||
from redis import StrictRedis
|
|
||||||
except ImportError:
|
|
||||||
print "The 'redis' python module is required, 'pip install redis'"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
class CacheModule(BaseCacheModule):
|
|
||||||
"""
|
|
||||||
A caching module backed by redis.
|
|
||||||
|
|
||||||
Keys are maintained in a zset with their score being the timestamp
|
|
||||||
when they are inserted. This allows for the usage of 'zremrangebyscore'
|
|
||||||
to expire keys. This mechanism is used or a pattern matched 'scan' for
|
|
||||||
performance.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if C.CACHE_PLUGIN_CONNECTION:
|
|
||||||
connection = C.CACHE_PLUGIN_CONNECTION.split(':')
|
|
||||||
else:
|
|
||||||
connection = []
|
|
||||||
|
|
||||||
self._timeout = float(C.CACHE_PLUGIN_TIMEOUT)
|
|
||||||
self._prefix = C.CACHE_PLUGIN_PREFIX
|
|
||||||
self._cache = StrictRedis(*connection)
|
|
||||||
self._keys_set = 'ansible_cache_keys'
|
|
||||||
|
|
||||||
def _make_key(self, key):
|
|
||||||
return self._prefix + key
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
value = self._cache.get(self._make_key(key))
|
|
||||||
# guard against the key not being removed from the zset;
|
|
||||||
# this could happen in cases where the timeout value is changed
|
|
||||||
# between invocations
|
|
||||||
if value is None:
|
|
||||||
self.delete(key)
|
|
||||||
raise KeyError
|
|
||||||
return json.loads(value)
|
|
||||||
|
|
||||||
def set(self, key, value):
|
|
||||||
value2 = jsonify(value)
|
|
||||||
if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
|
|
||||||
self._cache.setex(self._make_key(key), int(self._timeout), value2)
|
|
||||||
else:
|
|
||||||
self._cache.set(self._make_key(key), value2)
|
|
||||||
|
|
||||||
self._cache.zadd(self._keys_set, time.time(), key)
|
|
||||||
|
|
||||||
def _expire_keys(self):
|
|
||||||
if self._timeout > 0:
|
|
||||||
expiry_age = time.time() - self._timeout
|
|
||||||
self._cache.zremrangebyscore(self._keys_set, 0, expiry_age)
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
self._expire_keys()
|
|
||||||
return self._cache.zrange(self._keys_set, 0, -1)
|
|
||||||
|
|
||||||
def contains(self, key):
|
|
||||||
self._expire_keys()
|
|
||||||
return (self._cache.zrank(self._keys_set, key) >= 0)
|
|
||||||
|
|
||||||
def delete(self, key):
|
|
||||||
self._cache.delete(self._make_key(key))
|
|
||||||
self._cache.zrem(self._keys_set, key)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
for key in self.keys():
|
|
||||||
self.delete(key)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
# FIXME: there is probably a better way to do this in redis
|
|
||||||
ret = dict()
|
|
||||||
for key in self.keys():
|
|
||||||
ret[key] = self.get(key)
|
|
||||||
return ret
|
|
@ -1,94 +0,0 @@
|
|||||||
# (C) 2012-2014, Michael DeHaan, <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
this is an example ansible callback file that does nothing. You can drop
|
|
||||||
other classes in the same directory to define your own handlers. Methods
|
|
||||||
you do not use can be omitted. If self.disabled is set to True, the plugin
|
|
||||||
methods will not be called.
|
|
||||||
|
|
||||||
example uses include: logging, emailing, storing info, etc
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
#if foo:
|
|
||||||
# self.disabled = True
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_any(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_failed(self, host, res, ignore_errors=False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_ok(self, host, res):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_skipped(self, host, item=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_unreachable(self, host, res):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_no_hosts(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_async_poll(self, host, res, jid, clock):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_async_ok(self, host, res, jid):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def runner_on_async_failed(self, host, res, jid):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_start(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_notify(self, host, handler):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_no_hosts_matched(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_no_hosts_remaining(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_task_start(self, name, is_conditional):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_setup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_import_for_host(self, host, imported_file):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_not_import_for_host(self, host, missing_file):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_play_start(self, name):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def playbook_on_stats(self, stats):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,729 +0,0 @@
|
|||||||
# (C) 2012-2014, Michael DeHaan, <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import utils
|
|
||||||
import sys
|
|
||||||
import getpass
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import random
|
|
||||||
import fnmatch
|
|
||||||
import tempfile
|
|
||||||
import fcntl
|
|
||||||
import constants
|
|
||||||
import locale
|
|
||||||
from ansible.color import stringc
|
|
||||||
from ansible.module_utils import basic
|
|
||||||
from ansible.utils.unicode import to_unicode, to_bytes
|
|
||||||
|
|
||||||
import logging
|
|
||||||
if constants.DEFAULT_LOG_PATH != '':
|
|
||||||
path = constants.DEFAULT_LOG_PATH
|
|
||||||
|
|
||||||
if (os.path.exists(path) and not os.access(path, os.W_OK)) and not os.access(os.path.dirname(path), os.W_OK):
|
|
||||||
sys.stderr.write("log file at %s is not writeable, aborting\n" % path)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(filename=path, level=logging.DEBUG, format='%(asctime)s %(name)s %(message)s')
|
|
||||||
mypid = str(os.getpid())
|
|
||||||
user = getpass.getuser()
|
|
||||||
logger = logging.getLogger("p=%s u=%s | " % (mypid, user))
|
|
||||||
|
|
||||||
callback_plugins = []
|
|
||||||
|
|
||||||
def load_callback_plugins():
|
|
||||||
global callback_plugins
|
|
||||||
callback_plugins = [x for x in utils.plugins.callback_loader.all()]
|
|
||||||
|
|
||||||
def get_cowsay_info():
|
|
||||||
if constants.ANSIBLE_NOCOWS:
|
|
||||||
return (None, None)
|
|
||||||
cowsay = None
|
|
||||||
if os.path.exists("/usr/bin/cowsay"):
|
|
||||||
cowsay = "/usr/bin/cowsay"
|
|
||||||
elif os.path.exists("/usr/games/cowsay"):
|
|
||||||
cowsay = "/usr/games/cowsay"
|
|
||||||
elif os.path.exists("/usr/local/bin/cowsay"):
|
|
||||||
# BSD path for cowsay
|
|
||||||
cowsay = "/usr/local/bin/cowsay"
|
|
||||||
elif os.path.exists("/opt/local/bin/cowsay"):
|
|
||||||
# MacPorts path for cowsay
|
|
||||||
cowsay = "/opt/local/bin/cowsay"
|
|
||||||
|
|
||||||
noncow = os.getenv("ANSIBLE_COW_SELECTION",None)
|
|
||||||
if cowsay and noncow == 'random':
|
|
||||||
cmd = subprocess.Popen([cowsay, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
(out, err) = cmd.communicate()
|
|
||||||
cows = out.split()
|
|
||||||
cows.append(False)
|
|
||||||
noncow = random.choice(cows)
|
|
||||||
return (cowsay, noncow)
|
|
||||||
|
|
||||||
cowsay, noncow = get_cowsay_info()
|
|
||||||
|
|
||||||
def log_lockfile():
|
|
||||||
# create the path for the lockfile and open it
|
|
||||||
tempdir = tempfile.gettempdir()
|
|
||||||
uid = os.getuid()
|
|
||||||
path = os.path.join(tempdir, ".ansible-lock.%s" % uid)
|
|
||||||
lockfile = open(path, 'w')
|
|
||||||
# use fcntl to set FD_CLOEXEC on the file descriptor,
|
|
||||||
# so that we don't leak the file descriptor later
|
|
||||||
lockfile_fd = lockfile.fileno()
|
|
||||||
old_flags = fcntl.fcntl(lockfile_fd, fcntl.F_GETFD)
|
|
||||||
fcntl.fcntl(lockfile_fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
|
|
||||||
return lockfile
|
|
||||||
|
|
||||||
LOG_LOCK = log_lockfile()
|
|
||||||
|
|
||||||
def log_flock(runner):
|
|
||||||
if runner is not None:
|
|
||||||
try:
|
|
||||||
fcntl.lockf(runner.output_lockfile, fcntl.LOCK_EX)
|
|
||||||
except OSError:
|
|
||||||
# already got closed?
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
fcntl.lockf(LOG_LOCK, fcntl.LOCK_EX)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def log_unflock(runner):
|
|
||||||
if runner is not None:
|
|
||||||
try:
|
|
||||||
fcntl.lockf(runner.output_lockfile, fcntl.LOCK_UN)
|
|
||||||
except OSError:
|
|
||||||
# already got closed?
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
fcntl.lockf(LOG_LOCK, fcntl.LOCK_UN)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_playbook(callback, playbook):
|
|
||||||
''' used to notify callback plugins of playbook context '''
|
|
||||||
callback.playbook = playbook
|
|
||||||
for callback_plugin in callback_plugins:
|
|
||||||
callback_plugin.playbook = playbook
|
|
||||||
|
|
||||||
def set_play(callback, play):
|
|
||||||
''' used to notify callback plugins of context '''
|
|
||||||
callback.play = play
|
|
||||||
for callback_plugin in callback_plugins:
|
|
||||||
callback_plugin.play = play
|
|
||||||
|
|
||||||
def set_task(callback, task):
|
|
||||||
''' used to notify callback plugins of context '''
|
|
||||||
callback.task = task
|
|
||||||
for callback_plugin in callback_plugins:
|
|
||||||
callback_plugin.task = task
|
|
||||||
|
|
||||||
def display(msg, color=None, stderr=False, screen_only=False, log_only=False, runner=None):
|
|
||||||
# prevent a very rare case of interlaced multiprocess I/O
|
|
||||||
log_flock(runner)
|
|
||||||
msg2 = msg
|
|
||||||
if color:
|
|
||||||
msg2 = stringc(msg, color)
|
|
||||||
if not log_only:
|
|
||||||
if not stderr:
|
|
||||||
try:
|
|
||||||
print msg2
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
print msg2.encode('utf-8')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
print >>sys.stderr, msg2
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
print >>sys.stderr, msg2.encode('utf-8')
|
|
||||||
if constants.DEFAULT_LOG_PATH != '':
|
|
||||||
while msg.startswith("\n"):
|
|
||||||
msg = msg.replace("\n","")
|
|
||||||
if not screen_only:
|
|
||||||
if color == 'red':
|
|
||||||
logger.error(msg)
|
|
||||||
else:
|
|
||||||
logger.info(msg)
|
|
||||||
log_unflock(runner)
|
|
||||||
|
|
||||||
def call_callback_module(method_name, *args, **kwargs):
|
|
||||||
|
|
||||||
for callback_plugin in callback_plugins:
|
|
||||||
# a plugin that set self.disabled to True will not be called
|
|
||||||
# see osx_say.py example for such a plugin
|
|
||||||
if getattr(callback_plugin, 'disabled', False):
|
|
||||||
continue
|
|
||||||
methods = [
|
|
||||||
getattr(callback_plugin, method_name, None),
|
|
||||||
getattr(callback_plugin, 'on_any', None)
|
|
||||||
]
|
|
||||||
for method in methods:
|
|
||||||
if method is not None:
|
|
||||||
method(*args, **kwargs)
|
|
||||||
|
|
||||||
def vv(msg, host=None):
|
|
||||||
return verbose(msg, host=host, caplevel=1)
|
|
||||||
|
|
||||||
def vvv(msg, host=None):
|
|
||||||
return verbose(msg, host=host, caplevel=2)
|
|
||||||
|
|
||||||
def vvvv(msg, host=None):
|
|
||||||
return verbose(msg, host=host, caplevel=3)
|
|
||||||
|
|
||||||
def verbose(msg, host=None, caplevel=2):
|
|
||||||
msg = utils.sanitize_output(msg)
|
|
||||||
if utils.VERBOSITY > caplevel:
|
|
||||||
if host is None:
|
|
||||||
display(msg, color='blue')
|
|
||||||
else:
|
|
||||||
display("<%s> %s" % (host, msg), color='blue')
|
|
||||||
|
|
||||||
class AggregateStats(object):
|
|
||||||
''' holds stats about per-host activity during playbook runs '''
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
self.processed = {}
|
|
||||||
self.failures = {}
|
|
||||||
self.ok = {}
|
|
||||||
self.dark = {}
|
|
||||||
self.changed = {}
|
|
||||||
self.skipped = {}
|
|
||||||
|
|
||||||
def _increment(self, what, host):
|
|
||||||
''' helper function to bump a statistic '''
|
|
||||||
|
|
||||||
self.processed[host] = 1
|
|
||||||
prev = (getattr(self, what)).get(host, 0)
|
|
||||||
getattr(self, what)[host] = prev+1
|
|
||||||
|
|
||||||
def compute(self, runner_results, setup=False, poll=False, ignore_errors=False):
|
|
||||||
''' walk through all results and increment stats '''
|
|
||||||
|
|
||||||
for (host, value) in runner_results.get('contacted', {}).iteritems():
|
|
||||||
if not ignore_errors and (('failed' in value and bool(value['failed'])) or
|
|
||||||
('failed_when_result' in value and [value['failed_when_result']] or ['rc' in value and value['rc'] != 0])[0]):
|
|
||||||
self._increment('failures', host)
|
|
||||||
elif 'skipped' in value and bool(value['skipped']):
|
|
||||||
self._increment('skipped', host)
|
|
||||||
elif 'changed' in value and bool(value['changed']):
|
|
||||||
if not setup and not poll:
|
|
||||||
self._increment('changed', host)
|
|
||||||
self._increment('ok', host)
|
|
||||||
else:
|
|
||||||
if not poll or ('finished' in value and bool(value['finished'])):
|
|
||||||
self._increment('ok', host)
|
|
||||||
|
|
||||||
for (host, value) in runner_results.get('dark', {}).iteritems():
|
|
||||||
self._increment('dark', host)
|
|
||||||
|
|
||||||
|
|
||||||
def summarize(self, host):
|
|
||||||
''' return information about a particular host '''
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
ok = self.ok.get(host, 0),
|
|
||||||
failures = self.failures.get(host, 0),
|
|
||||||
unreachable = self.dark.get(host,0),
|
|
||||||
changed = self.changed.get(host, 0),
|
|
||||||
skipped = self.skipped.get(host, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
def regular_generic_msg(hostname, result, oneline, caption):
|
|
||||||
''' output on the result of a module run that is not command '''
|
|
||||||
|
|
||||||
if not oneline:
|
|
||||||
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result,format=True))
|
|
||||||
else:
|
|
||||||
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result))
|
|
||||||
|
|
||||||
|
|
||||||
def banner_cowsay(msg):
|
|
||||||
|
|
||||||
if ": [" in msg:
|
|
||||||
msg = msg.replace("[","")
|
|
||||||
if msg.endswith("]"):
|
|
||||||
msg = msg[:-1]
|
|
||||||
runcmd = [cowsay,"-W", "60"]
|
|
||||||
if noncow:
|
|
||||||
runcmd.append('-f')
|
|
||||||
runcmd.append(noncow)
|
|
||||||
runcmd.append(msg)
|
|
||||||
cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
(out, err) = cmd.communicate()
|
|
||||||
return "%s\n" % out
|
|
||||||
|
|
||||||
def banner_normal(msg):
|
|
||||||
|
|
||||||
width = 78 - len(msg)
|
|
||||||
if width < 3:
|
|
||||||
width = 3
|
|
||||||
filler = "*" * width
|
|
||||||
return "\n%s %s " % (msg, filler)
|
|
||||||
|
|
||||||
def banner(msg):
|
|
||||||
if cowsay:
|
|
||||||
try:
|
|
||||||
return banner_cowsay(msg)
|
|
||||||
except OSError:
|
|
||||||
# somebody cleverly deleted cowsay or something during the PB run. heh.
|
|
||||||
return banner_normal(msg)
|
|
||||||
return banner_normal(msg)
|
|
||||||
|
|
||||||
def command_generic_msg(hostname, result, oneline, caption):
|
|
||||||
''' output the result of a command run '''
|
|
||||||
|
|
||||||
rc = result.get('rc', '0')
|
|
||||||
stdout = result.get('stdout','')
|
|
||||||
stderr = result.get('stderr', '')
|
|
||||||
msg = result.get('msg', '')
|
|
||||||
|
|
||||||
hostname = hostname.encode('utf-8')
|
|
||||||
caption = caption.encode('utf-8')
|
|
||||||
|
|
||||||
if not oneline:
|
|
||||||
buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0))
|
|
||||||
if stdout:
|
|
||||||
buf += stdout
|
|
||||||
if stderr:
|
|
||||||
buf += stderr
|
|
||||||
if msg:
|
|
||||||
buf += msg
|
|
||||||
return buf + "\n"
|
|
||||||
else:
|
|
||||||
if stderr:
|
|
||||||
return "%s | %s | rc=%s | (stdout) %s (stderr) %s" % (hostname, caption, rc, stdout, stderr)
|
|
||||||
else:
|
|
||||||
return "%s | %s | rc=%s | (stdout) %s" % (hostname, caption, rc, stdout)
|
|
||||||
|
|
||||||
def host_report_msg(hostname, module_name, result, oneline):
|
|
||||||
''' summarize the JSON results for a particular host '''
|
|
||||||
|
|
||||||
failed = utils.is_failed(result)
|
|
||||||
msg = ('', None)
|
|
||||||
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result and result.get('parsed',True) != False:
|
|
||||||
if not failed:
|
|
||||||
msg = (command_generic_msg(hostname, result, oneline, 'success'), 'green')
|
|
||||||
else:
|
|
||||||
msg = (command_generic_msg(hostname, result, oneline, 'FAILED'), 'red')
|
|
||||||
else:
|
|
||||||
if not failed:
|
|
||||||
msg = (regular_generic_msg(hostname, result, oneline, 'success'), 'green')
|
|
||||||
else:
|
|
||||||
msg = (regular_generic_msg(hostname, result, oneline, 'FAILED'), 'red')
|
|
||||||
return msg
|
|
||||||
|
|
||||||
###############################################
|
|
||||||
|
|
||||||
class DefaultRunnerCallbacks(object):
|
|
||||||
''' no-op callbacks for API usage of Runner() if no callbacks are specified '''
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_failed(self, host, res, ignore_errors=False):
|
|
||||||
call_callback_module('runner_on_failed', host, res, ignore_errors=ignore_errors)
|
|
||||||
|
|
||||||
def on_ok(self, host, res):
|
|
||||||
call_callback_module('runner_on_ok', host, res)
|
|
||||||
|
|
||||||
def on_skipped(self, host, item=None):
|
|
||||||
call_callback_module('runner_on_skipped', host, item=item)
|
|
||||||
|
|
||||||
def on_unreachable(self, host, res):
|
|
||||||
call_callback_module('runner_on_unreachable', host, res)
|
|
||||||
|
|
||||||
def on_no_hosts(self):
|
|
||||||
call_callback_module('runner_on_no_hosts')
|
|
||||||
|
|
||||||
def on_async_poll(self, host, res, jid, clock):
|
|
||||||
call_callback_module('runner_on_async_poll', host, res, jid, clock)
|
|
||||||
|
|
||||||
def on_async_ok(self, host, res, jid):
|
|
||||||
call_callback_module('runner_on_async_ok', host, res, jid)
|
|
||||||
|
|
||||||
def on_async_failed(self, host, res, jid):
|
|
||||||
call_callback_module('runner_on_async_failed', host, res, jid)
|
|
||||||
|
|
||||||
def on_file_diff(self, host, diff):
|
|
||||||
call_callback_module('runner_on_file_diff', host, diff)
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
class CliRunnerCallbacks(DefaultRunnerCallbacks):
|
|
||||||
''' callbacks for use by /usr/bin/ansible '''
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# set by /usr/bin/ansible later
|
|
||||||
self.options = None
|
|
||||||
self._async_notified = {}
|
|
||||||
|
|
||||||
def on_failed(self, host, res, ignore_errors=False):
|
|
||||||
self._on_any(host,res)
|
|
||||||
super(CliRunnerCallbacks, self).on_failed(host, res, ignore_errors=ignore_errors)
|
|
||||||
|
|
||||||
def on_ok(self, host, res):
|
|
||||||
# hide magic variables used for ansible-playbook
|
|
||||||
res.pop('verbose_override', None)
|
|
||||||
res.pop('verbose_always', None)
|
|
||||||
|
|
||||||
self._on_any(host,res)
|
|
||||||
super(CliRunnerCallbacks, self).on_ok(host, res)
|
|
||||||
|
|
||||||
def on_unreachable(self, host, res):
|
|
||||||
if type(res) == dict:
|
|
||||||
res = res.get('msg','')
|
|
||||||
display("%s | FAILED => %s" % (host, res), stderr=True, color='red', runner=self.runner)
|
|
||||||
if self.options.tree:
|
|
||||||
utils.write_tree_file(
|
|
||||||
self.options.tree, host,
|
|
||||||
utils.jsonify(dict(failed=True, msg=res),format=True)
|
|
||||||
)
|
|
||||||
super(CliRunnerCallbacks, self).on_unreachable(host, res)
|
|
||||||
|
|
||||||
def on_skipped(self, host, item=None):
|
|
||||||
display("%s | skipped" % (host), runner=self.runner)
|
|
||||||
super(CliRunnerCallbacks, self).on_skipped(host, item)
|
|
||||||
|
|
||||||
def on_no_hosts(self):
|
|
||||||
display("no hosts matched\n", stderr=True, runner=self.runner)
|
|
||||||
super(CliRunnerCallbacks, self).on_no_hosts()
|
|
||||||
|
|
||||||
def on_async_poll(self, host, res, jid, clock):
|
|
||||||
if jid not in self._async_notified:
|
|
||||||
self._async_notified[jid] = clock + 1
|
|
||||||
if self._async_notified[jid] > clock:
|
|
||||||
self._async_notified[jid] = clock
|
|
||||||
display("<job %s> polling on %s, %ss remaining" % (jid, host, clock), runner=self.runner)
|
|
||||||
super(CliRunnerCallbacks, self).on_async_poll(host, res, jid, clock)
|
|
||||||
|
|
||||||
def on_async_ok(self, host, res, jid):
|
|
||||||
if jid:
|
|
||||||
display("<job %s> finished on %s => %s"%(jid, host, utils.jsonify(res,format=True)), runner=self.runner)
|
|
||||||
super(CliRunnerCallbacks, self).on_async_ok(host, res, jid)
|
|
||||||
|
|
||||||
def on_async_failed(self, host, res, jid):
|
|
||||||
display("<job %s> FAILED on %s => %s"%(jid, host, utils.jsonify(res,format=True)), color='red', stderr=True, runner=self.runner)
|
|
||||||
super(CliRunnerCallbacks, self).on_async_failed(host,res,jid)
|
|
||||||
|
|
||||||
def _on_any(self, host, result):
|
|
||||||
result2 = result.copy()
|
|
||||||
result2.pop('invocation', None)
|
|
||||||
(msg, color) = host_report_msg(host, self.options.module_name, result2, self.options.one_line)
|
|
||||||
display(msg, color=color, runner=self.runner)
|
|
||||||
if self.options.tree:
|
|
||||||
utils.write_tree_file(self.options.tree, host, utils.jsonify(result2,format=True))
|
|
||||||
|
|
||||||
def on_file_diff(self, host, diff):
|
|
||||||
display(utils.get_diff(diff), runner=self.runner)
|
|
||||||
super(CliRunnerCallbacks, self).on_file_diff(host, diff)
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
|
|
||||||
''' callbacks used for Runner() from /usr/bin/ansible-playbook '''
|
|
||||||
|
|
||||||
def __init__(self, stats, verbose=None):
|
|
||||||
|
|
||||||
if verbose is None:
|
|
||||||
verbose = utils.VERBOSITY
|
|
||||||
|
|
||||||
self.verbose = verbose
|
|
||||||
self.stats = stats
|
|
||||||
self._async_notified = {}
|
|
||||||
|
|
||||||
def on_unreachable(self, host, results):
|
|
||||||
if self.runner.delegate_to:
|
|
||||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
|
||||||
|
|
||||||
item = None
|
|
||||||
if type(results) == dict:
|
|
||||||
item = results.get('item', None)
|
|
||||||
if isinstance(item, unicode):
|
|
||||||
item = utils.unicode.to_bytes(item)
|
|
||||||
results = basic.json_dict_unicode_to_bytes(results)
|
|
||||||
else:
|
|
||||||
results = utils.unicode.to_bytes(results)
|
|
||||||
host = utils.unicode.to_bytes(host)
|
|
||||||
if item:
|
|
||||||
msg = "fatal: [%s] => (item=%s) => %s" % (host, item, results)
|
|
||||||
else:
|
|
||||||
msg = "fatal: [%s] => %s" % (host, results)
|
|
||||||
display(msg, color='red', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_unreachable(host, results)
|
|
||||||
|
|
||||||
def on_failed(self, host, results, ignore_errors=False):
|
|
||||||
if self.runner.delegate_to:
|
|
||||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
|
||||||
|
|
||||||
results2 = results.copy()
|
|
||||||
results2.pop('invocation', None)
|
|
||||||
|
|
||||||
item = results2.get('item', None)
|
|
||||||
parsed = results2.get('parsed', True)
|
|
||||||
module_msg = ''
|
|
||||||
if not parsed:
|
|
||||||
module_msg = results2.pop('msg', None)
|
|
||||||
stderr = results2.pop('stderr', None)
|
|
||||||
stdout = results2.pop('stdout', None)
|
|
||||||
returned_msg = results2.pop('msg', None)
|
|
||||||
|
|
||||||
results2['task'] = self.task.name
|
|
||||||
results2['role'] = self.task.role_name
|
|
||||||
results2['playbook'] = self.playbook.filename
|
|
||||||
|
|
||||||
if item:
|
|
||||||
msg = "failed: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(results2))
|
|
||||||
else:
|
|
||||||
msg = "failed: [%s] => %s" % (host, utils.jsonify(results2))
|
|
||||||
display(msg, color='red', runner=self.runner)
|
|
||||||
|
|
||||||
if stderr:
|
|
||||||
display("stderr: %s" % stderr, color='red', runner=self.runner)
|
|
||||||
if stdout:
|
|
||||||
display("stdout: %s" % stdout, color='red', runner=self.runner)
|
|
||||||
if returned_msg:
|
|
||||||
display("msg: %s" % returned_msg, color='red', runner=self.runner)
|
|
||||||
if not parsed and module_msg:
|
|
||||||
display(module_msg, color='red', runner=self.runner)
|
|
||||||
if ignore_errors:
|
|
||||||
display("...ignoring", color='cyan', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_failed(host, results, ignore_errors=ignore_errors)
|
|
||||||
|
|
||||||
def on_ok(self, host, host_result):
|
|
||||||
if self.runner.delegate_to:
|
|
||||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
|
||||||
|
|
||||||
item = host_result.get('item', None)
|
|
||||||
|
|
||||||
host_result2 = host_result.copy()
|
|
||||||
host_result2.pop('invocation', None)
|
|
||||||
verbose_always = host_result2.pop('verbose_always', False)
|
|
||||||
changed = host_result.get('changed', False)
|
|
||||||
ok_or_changed = 'ok'
|
|
||||||
if changed:
|
|
||||||
ok_or_changed = 'changed'
|
|
||||||
|
|
||||||
# show verbose output for non-setup module results if --verbose is used
|
|
||||||
msg = ''
|
|
||||||
if (not self.verbose or host_result2.get("verbose_override",None) is not
|
|
||||||
None) and not verbose_always:
|
|
||||||
if item:
|
|
||||||
msg = "%s: [%s] => (item=%s)" % (ok_or_changed, host, item)
|
|
||||||
else:
|
|
||||||
if 'ansible_job_id' not in host_result or 'finished' in host_result:
|
|
||||||
msg = "%s: [%s]" % (ok_or_changed, host)
|
|
||||||
else:
|
|
||||||
# verbose ...
|
|
||||||
if item:
|
|
||||||
msg = "%s: [%s] => (item=%s) => %s" % (ok_or_changed, host, item, utils.jsonify(host_result2, format=verbose_always))
|
|
||||||
else:
|
|
||||||
if 'ansible_job_id' not in host_result or 'finished' in host_result2:
|
|
||||||
msg = "%s: [%s] => %s" % (ok_or_changed, host, utils.jsonify(host_result2, format=verbose_always))
|
|
||||||
|
|
||||||
if msg != '':
|
|
||||||
if not changed:
|
|
||||||
display(msg, color='green', runner=self.runner)
|
|
||||||
else:
|
|
||||||
display(msg, color='yellow', runner=self.runner)
|
|
||||||
if constants.COMMAND_WARNINGS and 'warnings' in host_result2 and host_result2['warnings']:
|
|
||||||
for warning in host_result2['warnings']:
|
|
||||||
display("warning: %s" % warning, color='purple', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_ok(host, host_result)
|
|
||||||
|
|
||||||
def on_skipped(self, host, item=None):
|
|
||||||
if self.runner.delegate_to:
|
|
||||||
host = '%s -> %s' % (host, self.runner.delegate_to)
|
|
||||||
|
|
||||||
if constants.DISPLAY_SKIPPED_HOSTS:
|
|
||||||
msg = ''
|
|
||||||
if item:
|
|
||||||
msg = "skipping: [%s] => (item=%s)" % (host, item)
|
|
||||||
else:
|
|
||||||
msg = "skipping: [%s]" % host
|
|
||||||
display(msg, color='cyan', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_skipped(host, item)
|
|
||||||
|
|
||||||
def on_no_hosts(self):
|
|
||||||
display("FATAL: no hosts matched or all hosts have already failed -- aborting\n", color='red', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_no_hosts()
|
|
||||||
|
|
||||||
def on_async_poll(self, host, res, jid, clock):
|
|
||||||
if jid not in self._async_notified:
|
|
||||||
self._async_notified[jid] = clock + 1
|
|
||||||
if self._async_notified[jid] > clock:
|
|
||||||
self._async_notified[jid] = clock
|
|
||||||
msg = "<job %s> polling, %ss remaining"%(jid, clock)
|
|
||||||
display(msg, color='cyan', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_async_poll(host,res,jid,clock)
|
|
||||||
|
|
||||||
def on_async_ok(self, host, res, jid):
|
|
||||||
if jid:
|
|
||||||
msg = "<job %s> finished on %s"%(jid, host)
|
|
||||||
display(msg, color='cyan', runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_async_ok(host, res, jid)
|
|
||||||
|
|
||||||
def on_async_failed(self, host, res, jid):
|
|
||||||
msg = "<job %s> FAILED on %s" % (jid, host)
|
|
||||||
display(msg, color='red', stderr=True, runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_async_failed(host,res,jid)
|
|
||||||
|
|
||||||
def on_file_diff(self, host, diff):
|
|
||||||
display(utils.get_diff(diff), runner=self.runner)
|
|
||||||
super(PlaybookRunnerCallbacks, self).on_file_diff(host, diff)
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
class PlaybookCallbacks(object):
|
|
||||||
''' playbook.py callbacks used by /usr/bin/ansible-playbook '''
|
|
||||||
|
|
||||||
def __init__(self, verbose=False):
|
|
||||||
|
|
||||||
self.verbose = verbose
|
|
||||||
|
|
||||||
def on_start(self):
|
|
||||||
call_callback_module('playbook_on_start')
|
|
||||||
|
|
||||||
def on_notify(self, host, handler):
|
|
||||||
call_callback_module('playbook_on_notify', host, handler)
|
|
||||||
|
|
||||||
def on_no_hosts_matched(self):
|
|
||||||
display("skipping: no hosts matched", color='cyan')
|
|
||||||
call_callback_module('playbook_on_no_hosts_matched')
|
|
||||||
|
|
||||||
def on_no_hosts_remaining(self):
|
|
||||||
display("\nFATAL: all hosts have already failed -- aborting", color='red')
|
|
||||||
call_callback_module('playbook_on_no_hosts_remaining')
|
|
||||||
|
|
||||||
def on_task_start(self, name, is_conditional):
|
|
||||||
name = utils.unicode.to_bytes(name)
|
|
||||||
msg = "TASK: [%s]" % name
|
|
||||||
if is_conditional:
|
|
||||||
msg = "NOTIFIED: [%s]" % name
|
|
||||||
|
|
||||||
if hasattr(self, 'start_at'):
|
|
||||||
self.start_at = utils.unicode.to_bytes(self.start_at)
|
|
||||||
if name == self.start_at or fnmatch.fnmatch(name, self.start_at):
|
|
||||||
# we found out match, we can get rid of this now
|
|
||||||
del self.start_at
|
|
||||||
elif self.task.role_name:
|
|
||||||
# handle tasks prefixed with rolenames
|
|
||||||
actual_name = name.split('|', 1)[1].lstrip()
|
|
||||||
if actual_name == self.start_at or fnmatch.fnmatch(actual_name, self.start_at):
|
|
||||||
del self.start_at
|
|
||||||
|
|
||||||
if hasattr(self, 'start_at'): # we still have start_at so skip the task
|
|
||||||
self.skip_task = True
|
|
||||||
elif hasattr(self, 'step') and self.step:
|
|
||||||
if isinstance(name, str):
|
|
||||||
name = utils.unicode.to_unicode(name)
|
|
||||||
msg = u'Perform task: %s (y/n/c): ' % name
|
|
||||||
if sys.stdout.encoding:
|
|
||||||
msg = to_bytes(msg, sys.stdout.encoding)
|
|
||||||
else:
|
|
||||||
msg = to_bytes(msg)
|
|
||||||
resp = raw_input(msg)
|
|
||||||
if resp.lower() in ['y','yes']:
|
|
||||||
self.skip_task = False
|
|
||||||
display(banner(msg))
|
|
||||||
elif resp.lower() in ['c', 'continue']:
|
|
||||||
self.skip_task = False
|
|
||||||
self.step = False
|
|
||||||
display(banner(msg))
|
|
||||||
else:
|
|
||||||
self.skip_task = True
|
|
||||||
else:
|
|
||||||
self.skip_task = False
|
|
||||||
display(banner(msg))
|
|
||||||
|
|
||||||
call_callback_module('playbook_on_task_start', name, is_conditional)
|
|
||||||
|
|
||||||
def on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
|
|
||||||
|
|
||||||
if prompt and default is not None:
|
|
||||||
msg = "%s [%s]: " % (prompt, default)
|
|
||||||
elif prompt:
|
|
||||||
msg = "%s: " % prompt
|
|
||||||
else:
|
|
||||||
msg = 'input for %s: ' % varname
|
|
||||||
|
|
||||||
def do_prompt(prompt, private):
|
|
||||||
if sys.stdout.encoding:
|
|
||||||
msg = prompt.encode(sys.stdout.encoding)
|
|
||||||
else:
|
|
||||||
# when piping the output, or at other times when stdout
|
|
||||||
# may not be the standard file descriptor, the stdout
|
|
||||||
# encoding may not be set, so default to something sane
|
|
||||||
msg = prompt.encode(locale.getpreferredencoding())
|
|
||||||
if private:
|
|
||||||
return getpass.getpass(msg)
|
|
||||||
return raw_input(msg)
|
|
||||||
|
|
||||||
|
|
||||||
if confirm:
|
|
||||||
while True:
|
|
||||||
result = do_prompt(msg, private)
|
|
||||||
second = do_prompt("confirm " + msg, private)
|
|
||||||
if result == second:
|
|
||||||
break
|
|
||||||
display("***** VALUES ENTERED DO NOT MATCH ****")
|
|
||||||
else:
|
|
||||||
result = do_prompt(msg, private)
|
|
||||||
|
|
||||||
# if result is false and default is not None
|
|
||||||
if not result and default is not None:
|
|
||||||
result = default
|
|
||||||
|
|
||||||
|
|
||||||
if encrypt:
|
|
||||||
result = utils.do_encrypt(result, encrypt, salt_size, salt)
|
|
||||||
|
|
||||||
# handle utf-8 chars
|
|
||||||
result = to_unicode(result, errors='strict')
|
|
||||||
call_callback_module( 'playbook_on_vars_prompt', varname, private=private, prompt=prompt,
|
|
||||||
encrypt=encrypt, confirm=confirm, salt_size=salt_size, salt=None, default=default
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def on_setup(self):
|
|
||||||
display(banner("GATHERING FACTS"))
|
|
||||||
call_callback_module('playbook_on_setup')
|
|
||||||
|
|
||||||
def on_import_for_host(self, host, imported_file):
|
|
||||||
msg = "%s: importing %s" % (host, imported_file)
|
|
||||||
display(msg, color='cyan')
|
|
||||||
call_callback_module('playbook_on_import_for_host', host, imported_file)
|
|
||||||
|
|
||||||
def on_not_import_for_host(self, host, missing_file):
|
|
||||||
msg = "%s: not importing file: %s" % (host, missing_file)
|
|
||||||
display(msg, color='cyan')
|
|
||||||
call_callback_module('playbook_on_not_import_for_host', host, missing_file)
|
|
||||||
|
|
||||||
def on_play_start(self, name):
|
|
||||||
display(banner("PLAY [%s]" % name))
|
|
||||||
call_callback_module('playbook_on_play_start', name)
|
|
||||||
|
|
||||||
def on_stats(self, stats):
|
|
||||||
call_callback_module('playbook_on_stats', stats)
|
|
||||||
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import constants
|
|
||||||
|
|
||||||
ANSIBLE_COLOR=True
|
|
||||||
if constants.ANSIBLE_NOCOLOR:
|
|
||||||
ANSIBLE_COLOR=False
|
|
||||||
elif not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty():
|
|
||||||
ANSIBLE_COLOR=False
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
import curses
|
|
||||||
curses.setupterm()
|
|
||||||
if curses.tigetnum('colors') < 0:
|
|
||||||
ANSIBLE_COLOR=False
|
|
||||||
except ImportError:
|
|
||||||
# curses library was not found
|
|
||||||
pass
|
|
||||||
except curses.error:
|
|
||||||
# curses returns an error (e.g. could not find terminal)
|
|
||||||
ANSIBLE_COLOR=False
|
|
||||||
|
|
||||||
if constants.ANSIBLE_FORCE_COLOR:
|
|
||||||
ANSIBLE_COLOR=True
|
|
||||||
|
|
||||||
# --- begin "pretty"
|
|
||||||
#
|
|
||||||
# pretty - A miniature library that provides a Python print and stdout
|
|
||||||
# wrapper that makes colored terminal text easier to use (e.g. without
|
|
||||||
# having to mess around with ANSI escape sequences). This code is public
|
|
||||||
# domain - there is no license except that you must leave this header.
|
|
||||||
#
|
|
||||||
# Copyright (C) 2008 Brian Nez <thedude at bri1 dot com>
|
|
||||||
#
|
|
||||||
# http://nezzen.net/2008/06/23/colored-text-in-python-using-ansi-escape-sequences/
|
|
||||||
|
|
||||||
codeCodes = {
|
|
||||||
'black': '0;30', 'bright gray': '0;37',
|
|
||||||
'blue': '0;34', 'white': '1;37',
|
|
||||||
'green': '0;32', 'bright blue': '1;34',
|
|
||||||
'cyan': '0;36', 'bright green': '1;32',
|
|
||||||
'red': '0;31', 'bright cyan': '1;36',
|
|
||||||
'purple': '0;35', 'bright red': '1;31',
|
|
||||||
'yellow': '0;33', 'bright purple': '1;35',
|
|
||||||
'dark gray': '1;30', 'bright yellow': '1;33',
|
|
||||||
'normal': '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
def stringc(text, color):
|
|
||||||
"""String in color."""
|
|
||||||
|
|
||||||
if ANSIBLE_COLOR:
|
|
||||||
return "\033["+codeCodes[color]+"m"+text+"\033[0m"
|
|
||||||
else:
|
|
||||||
return text
|
|
||||||
|
|
||||||
# --- end "pretty"
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pwd
|
|
||||||
import sys
|
|
||||||
import ConfigParser
|
|
||||||
from string import ascii_letters, digits
|
|
||||||
|
|
||||||
# copied from utils, avoid circular reference fun :)
|
|
||||||
def mk_boolean(value):
|
|
||||||
if value is None:
|
|
||||||
return False
|
|
||||||
val = str(value)
|
|
||||||
if val.lower() in [ "true", "t", "y", "1", "yes" ]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_config(p, section, key, env_var, default, boolean=False, integer=False, floating=False, islist=False):
|
|
||||||
''' return a configuration variable with casting '''
|
|
||||||
value = _get_config(p, section, key, env_var, default)
|
|
||||||
if boolean:
|
|
||||||
return mk_boolean(value)
|
|
||||||
if value and integer:
|
|
||||||
return int(value)
|
|
||||||
if value and floating:
|
|
||||||
return float(value)
|
|
||||||
if value and islist:
|
|
||||||
return [x.strip() for x in value.split(',')]
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _get_config(p, section, key, env_var, default):
|
|
||||||
''' helper function for get_config '''
|
|
||||||
if env_var is not None:
|
|
||||||
value = os.environ.get(env_var, None)
|
|
||||||
if value is not None:
|
|
||||||
return value
|
|
||||||
if p is not None:
|
|
||||||
try:
|
|
||||||
return p.get(section, key, raw=True)
|
|
||||||
except:
|
|
||||||
return default
|
|
||||||
return default
|
|
||||||
|
|
||||||
def load_config_file():
|
|
||||||
''' Load Config File order(first found is used): ENV, CWD, HOME, /etc/ansible '''
|
|
||||||
|
|
||||||
p = ConfigParser.ConfigParser()
|
|
||||||
|
|
||||||
path0 = os.getenv("ANSIBLE_CONFIG", None)
|
|
||||||
if path0 is not None:
|
|
||||||
path0 = os.path.expanduser(path0)
|
|
||||||
path1 = os.getcwd() + "/ansible.cfg"
|
|
||||||
path2 = os.path.expanduser("~/.ansible.cfg")
|
|
||||||
path3 = "/etc/ansible/ansible.cfg"
|
|
||||||
|
|
||||||
for path in [path0, path1, path2, path3]:
|
|
||||||
if path is not None and os.path.exists(path):
|
|
||||||
try:
|
|
||||||
p.read(path)
|
|
||||||
except ConfigParser.Error as e:
|
|
||||||
print "Error reading config file: \n%s" % e
|
|
||||||
sys.exit(1)
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
def shell_expand_path(path):
|
|
||||||
''' shell_expand_path is needed as os.path.expanduser does not work
|
|
||||||
when path is None, which is the default for ANSIBLE_PRIVATE_KEY_FILE '''
|
|
||||||
if path:
|
|
||||||
path = os.path.expanduser(os.path.expandvars(path))
|
|
||||||
return path
|
|
||||||
|
|
||||||
p = load_config_file()
|
|
||||||
|
|
||||||
active_user = pwd.getpwuid(os.geteuid())[0]
|
|
||||||
|
|
||||||
# check all of these extensions when looking for yaml files for things like
|
|
||||||
# group variables -- really anything we can load
|
|
||||||
YAML_FILENAME_EXTENSIONS = [ "", ".yml", ".yaml", ".json" ]
|
|
||||||
|
|
||||||
# sections in config file
|
|
||||||
DEFAULTS='defaults'
|
|
||||||
|
|
||||||
# configurable things
|
|
||||||
DEFAULT_HOST_LIST = shell_expand_path(get_config(p, DEFAULTS, 'inventory', 'ANSIBLE_INVENTORY', get_config(p, DEFAULTS,'hostfile','ANSIBLE_HOSTS', '/etc/ansible/hosts')))
|
|
||||||
DEFAULT_MODULE_PATH = get_config(p, DEFAULTS, 'library', 'ANSIBLE_LIBRARY', None)
|
|
||||||
DEFAULT_ROLES_PATH = shell_expand_path(get_config(p, DEFAULTS, 'roles_path', 'ANSIBLE_ROLES_PATH', '/etc/ansible/roles'))
|
|
||||||
DEFAULT_REMOTE_TMP = get_config(p, DEFAULTS, 'remote_tmp', 'ANSIBLE_REMOTE_TEMP', '$HOME/.ansible/tmp')
|
|
||||||
DEFAULT_MODULE_NAME = get_config(p, DEFAULTS, 'module_name', None, 'command')
|
|
||||||
DEFAULT_PATTERN = get_config(p, DEFAULTS, 'pattern', None, '*')
|
|
||||||
DEFAULT_FORKS = get_config(p, DEFAULTS, 'forks', 'ANSIBLE_FORKS', 5, integer=True)
|
|
||||||
DEFAULT_MODULE_ARGS = get_config(p, DEFAULTS, 'module_args', 'ANSIBLE_MODULE_ARGS', '')
|
|
||||||
DEFAULT_MODULE_LANG = get_config(p, DEFAULTS, 'module_lang', 'ANSIBLE_MODULE_LANG', 'en_US.UTF-8')
|
|
||||||
DEFAULT_TIMEOUT = get_config(p, DEFAULTS, 'timeout', 'ANSIBLE_TIMEOUT', 10, integer=True)
|
|
||||||
DEFAULT_POLL_INTERVAL = get_config(p, DEFAULTS, 'poll_interval', 'ANSIBLE_POLL_INTERVAL', 15, integer=True)
|
|
||||||
DEFAULT_REMOTE_USER = get_config(p, DEFAULTS, 'remote_user', 'ANSIBLE_REMOTE_USER', active_user)
|
|
||||||
DEFAULT_ASK_PASS = get_config(p, DEFAULTS, 'ask_pass', 'ANSIBLE_ASK_PASS', False, boolean=True)
|
|
||||||
DEFAULT_PRIVATE_KEY_FILE = shell_expand_path(get_config(p, DEFAULTS, 'private_key_file', 'ANSIBLE_PRIVATE_KEY_FILE', None))
|
|
||||||
DEFAULT_ASK_SUDO_PASS = get_config(p, DEFAULTS, 'ask_sudo_pass', 'ANSIBLE_ASK_SUDO_PASS', False, boolean=True)
|
|
||||||
DEFAULT_REMOTE_PORT = get_config(p, DEFAULTS, 'remote_port', 'ANSIBLE_REMOTE_PORT', None, integer=True)
|
|
||||||
DEFAULT_ASK_VAULT_PASS = get_config(p, DEFAULTS, 'ask_vault_pass', 'ANSIBLE_ASK_VAULT_PASS', False, boolean=True)
|
|
||||||
DEFAULT_VAULT_PASSWORD_FILE = shell_expand_path(get_config(p, DEFAULTS, 'vault_password_file', 'ANSIBLE_VAULT_PASSWORD_FILE', None))
|
|
||||||
DEFAULT_TRANSPORT = get_config(p, DEFAULTS, 'transport', 'ANSIBLE_TRANSPORT', 'smart')
|
|
||||||
DEFAULT_SCP_IF_SSH = get_config(p, 'ssh_connection', 'scp_if_ssh', 'ANSIBLE_SCP_IF_SSH', False, boolean=True)
|
|
||||||
DEFAULT_MANAGED_STR = get_config(p, DEFAULTS, 'ansible_managed', None, 'Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}')
|
|
||||||
DEFAULT_SYSLOG_FACILITY = get_config(p, DEFAULTS, 'syslog_facility', 'ANSIBLE_SYSLOG_FACILITY', 'LOG_USER')
|
|
||||||
DEFAULT_KEEP_REMOTE_FILES = get_config(p, DEFAULTS, 'keep_remote_files', 'ANSIBLE_KEEP_REMOTE_FILES', False, boolean=True)
|
|
||||||
DEFAULT_SUDO = get_config(p, DEFAULTS, 'sudo', 'ANSIBLE_SUDO', False, boolean=True)
|
|
||||||
DEFAULT_SUDO_USER = get_config(p, DEFAULTS, 'sudo_user', 'ANSIBLE_SUDO_USER', 'root')
|
|
||||||
DEFAULT_SUDO_EXE = get_config(p, DEFAULTS, 'sudo_exe', 'ANSIBLE_SUDO_EXE', 'sudo')
|
|
||||||
DEFAULT_SUDO_FLAGS = get_config(p, DEFAULTS, 'sudo_flags', 'ANSIBLE_SUDO_FLAGS', '-H')
|
|
||||||
DEFAULT_HASH_BEHAVIOUR = get_config(p, DEFAULTS, 'hash_behaviour', 'ANSIBLE_HASH_BEHAVIOUR', 'replace')
|
|
||||||
DEFAULT_JINJA2_EXTENSIONS = get_config(p, DEFAULTS, 'jinja2_extensions', 'ANSIBLE_JINJA2_EXTENSIONS', None)
|
|
||||||
DEFAULT_EXECUTABLE = get_config(p, DEFAULTS, 'executable', 'ANSIBLE_EXECUTABLE', '/bin/sh')
|
|
||||||
DEFAULT_SU_EXE = get_config(p, DEFAULTS, 'su_exe', 'ANSIBLE_SU_EXE', 'su')
|
|
||||||
DEFAULT_SU = get_config(p, DEFAULTS, 'su', 'ANSIBLE_SU', False, boolean=True)
|
|
||||||
DEFAULT_SU_FLAGS = get_config(p, DEFAULTS, 'su_flags', 'ANSIBLE_SU_FLAGS', '')
|
|
||||||
DEFAULT_SU_USER = get_config(p, DEFAULTS, 'su_user', 'ANSIBLE_SU_USER', 'root')
|
|
||||||
DEFAULT_ASK_SU_PASS = get_config(p, DEFAULTS, 'ask_su_pass', 'ANSIBLE_ASK_SU_PASS', False, boolean=True)
|
|
||||||
DEFAULT_GATHERING = get_config(p, DEFAULTS, 'gathering', 'ANSIBLE_GATHERING', 'implicit').lower()
|
|
||||||
DEFAULT_LOG_PATH = shell_expand_path(get_config(p, DEFAULTS, 'log_path', 'ANSIBLE_LOG_PATH', ''))
|
|
||||||
|
|
||||||
# selinux
|
|
||||||
DEFAULT_SELINUX_SPECIAL_FS = get_config(p, 'selinux', 'special_context_filesystems', None, 'fuse, nfs, vboxsf', islist=True)
|
|
||||||
|
|
||||||
#TODO: get rid of ternary chain mess
|
|
||||||
BECOME_METHODS = ['sudo','su','pbrun','pfexec','runas']
|
|
||||||
BECOME_ERROR_STRINGS = {'sudo': 'Sorry, try again.', 'su': 'Authentication failure', 'pbrun': '', 'pfexec': '', 'runas': ''}
|
|
||||||
DEFAULT_BECOME = get_config(p, 'privilege_escalation', 'become', 'ANSIBLE_BECOME',False, boolean=True)
|
|
||||||
DEFAULT_BECOME_METHOD = get_config(p, 'privilege_escalation', 'become_method', 'ANSIBLE_BECOME_METHOD','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo' ).lower()
|
|
||||||
DEFAULT_BECOME_USER = get_config(p, 'privilege_escalation', 'become_user', 'ANSIBLE_BECOME_USER',default=None)
|
|
||||||
DEFAULT_BECOME_ASK_PASS = get_config(p, 'privilege_escalation', 'become_ask_pass', 'ANSIBLE_BECOME_ASK_PASS', False, boolean=True)
|
|
||||||
# need to rethink impementing these 2
|
|
||||||
DEFAULT_BECOME_EXE = None
|
|
||||||
#DEFAULT_BECOME_EXE = get_config(p, DEFAULTS, 'become_exe', 'ANSIBLE_BECOME_EXE','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo')
|
|
||||||
#DEFAULT_BECOME_FLAGS = get_config(p, DEFAULTS, 'become_flags', 'ANSIBLE_BECOME_FLAGS',DEFAULT_SUDO_FLAGS if DEFAULT_SUDO else DEFAULT_SU_FLAGS if DEFAULT_SU else '-H')
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ACTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'action_plugins', 'ANSIBLE_ACTION_PLUGINS', '~/.ansible/plugins/action_plugins:/usr/share/ansible_plugins/action_plugins')
|
|
||||||
DEFAULT_CACHE_PLUGIN_PATH = get_config(p, DEFAULTS, 'cache_plugins', 'ANSIBLE_CACHE_PLUGINS', '~/.ansible/plugins/cache_plugins:/usr/share/ansible_plugins/cache_plugins')
|
|
||||||
DEFAULT_CALLBACK_PLUGIN_PATH = get_config(p, DEFAULTS, 'callback_plugins', 'ANSIBLE_CALLBACK_PLUGINS', '~/.ansible/plugins/callback_plugins:/usr/share/ansible_plugins/callback_plugins')
|
|
||||||
DEFAULT_CONNECTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'connection_plugins', 'ANSIBLE_CONNECTION_PLUGINS', '~/.ansible/plugins/connection_plugins:/usr/share/ansible_plugins/connection_plugins')
|
|
||||||
DEFAULT_LOOKUP_PLUGIN_PATH = get_config(p, DEFAULTS, 'lookup_plugins', 'ANSIBLE_LOOKUP_PLUGINS', '~/.ansible/plugins/lookup_plugins:/usr/share/ansible_plugins/lookup_plugins')
|
|
||||||
DEFAULT_VARS_PLUGIN_PATH = get_config(p, DEFAULTS, 'vars_plugins', 'ANSIBLE_VARS_PLUGINS', '~/.ansible/plugins/vars_plugins:/usr/share/ansible_plugins/vars_plugins')
|
|
||||||
DEFAULT_FILTER_PLUGIN_PATH = get_config(p, DEFAULTS, 'filter_plugins', 'ANSIBLE_FILTER_PLUGINS', '~/.ansible/plugins/filter_plugins:/usr/share/ansible_plugins/filter_plugins')
|
|
||||||
|
|
||||||
CACHE_PLUGIN = get_config(p, DEFAULTS, 'fact_caching', 'ANSIBLE_CACHE_PLUGIN', 'memory')
|
|
||||||
CACHE_PLUGIN_CONNECTION = get_config(p, DEFAULTS, 'fact_caching_connection', 'ANSIBLE_CACHE_PLUGIN_CONNECTION', None)
|
|
||||||
CACHE_PLUGIN_PREFIX = get_config(p, DEFAULTS, 'fact_caching_prefix', 'ANSIBLE_CACHE_PLUGIN_PREFIX', 'ansible_facts')
|
|
||||||
CACHE_PLUGIN_TIMEOUT = get_config(p, DEFAULTS, 'fact_caching_timeout', 'ANSIBLE_CACHE_PLUGIN_TIMEOUT', 24 * 60 * 60, integer=True)
|
|
||||||
|
|
||||||
ANSIBLE_FORCE_COLOR = get_config(p, DEFAULTS, 'force_color', 'ANSIBLE_FORCE_COLOR', None, boolean=True)
|
|
||||||
ANSIBLE_NOCOLOR = get_config(p, DEFAULTS, 'nocolor', 'ANSIBLE_NOCOLOR', None, boolean=True)
|
|
||||||
ANSIBLE_NOCOWS = get_config(p, DEFAULTS, 'nocows', 'ANSIBLE_NOCOWS', None, boolean=True)
|
|
||||||
DISPLAY_SKIPPED_HOSTS = get_config(p, DEFAULTS, 'display_skipped_hosts', 'DISPLAY_SKIPPED_HOSTS', True, boolean=True)
|
|
||||||
DEFAULT_UNDEFINED_VAR_BEHAVIOR = get_config(p, DEFAULTS, 'error_on_undefined_vars', 'ANSIBLE_ERROR_ON_UNDEFINED_VARS', True, boolean=True)
|
|
||||||
HOST_KEY_CHECKING = get_config(p, DEFAULTS, 'host_key_checking', 'ANSIBLE_HOST_KEY_CHECKING', True, boolean=True)
|
|
||||||
SYSTEM_WARNINGS = get_config(p, DEFAULTS, 'system_warnings', 'ANSIBLE_SYSTEM_WARNINGS', True, boolean=True)
|
|
||||||
DEPRECATION_WARNINGS = get_config(p, DEFAULTS, 'deprecation_warnings', 'ANSIBLE_DEPRECATION_WARNINGS', True, boolean=True)
|
|
||||||
DEFAULT_CALLABLE_WHITELIST = get_config(p, DEFAULTS, 'callable_whitelist', 'ANSIBLE_CALLABLE_WHITELIST', [], islist=True)
|
|
||||||
COMMAND_WARNINGS = get_config(p, DEFAULTS, 'command_warnings', 'ANSIBLE_COMMAND_WARNINGS', False, boolean=True)
|
|
||||||
DEFAULT_LOAD_CALLBACK_PLUGINS = get_config(p, DEFAULTS, 'bin_ansible_callbacks', 'ANSIBLE_LOAD_CALLBACK_PLUGINS', False, boolean=True)
|
|
||||||
DEFAULT_FORCE_HANDLERS = get_config(p, DEFAULTS, 'force_handlers', 'ANSIBLE_FORCE_HANDLERS', False, boolean=True)
|
|
||||||
|
|
||||||
|
|
||||||
RETRY_FILES_ENABLED = get_config(p, DEFAULTS, 'retry_files_enabled', 'ANSIBLE_RETRY_FILES_ENABLED', True, boolean=True)
|
|
||||||
RETRY_FILES_SAVE_PATH = get_config(p, DEFAULTS, 'retry_files_save_path', 'ANSIBLE_RETRY_FILES_SAVE_PATH', '~/')
|
|
||||||
|
|
||||||
# CONNECTION RELATED
|
|
||||||
ANSIBLE_SSH_ARGS = get_config(p, 'ssh_connection', 'ssh_args', 'ANSIBLE_SSH_ARGS', None)
|
|
||||||
ANSIBLE_SSH_CONTROL_PATH = get_config(p, 'ssh_connection', 'control_path', 'ANSIBLE_SSH_CONTROL_PATH', "%(directory)s/ansible-ssh-%%h-%%p-%%r")
|
|
||||||
ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, boolean=True)
|
|
||||||
PARAMIKO_RECORD_HOST_KEYS = get_config(p, 'paramiko_connection', 'record_host_keys', 'ANSIBLE_PARAMIKO_RECORD_HOST_KEYS', True, boolean=True)
|
|
||||||
# obsolete -- will be formally removed
|
|
||||||
ZEROMQ_PORT = get_config(p, 'fireball_connection', 'zeromq_port', 'ANSIBLE_ZEROMQ_PORT', 5099, integer=True)
|
|
||||||
ACCELERATE_PORT = get_config(p, 'accelerate', 'accelerate_port', 'ACCELERATE_PORT', 5099, integer=True)
|
|
||||||
ACCELERATE_TIMEOUT = get_config(p, 'accelerate', 'accelerate_timeout', 'ACCELERATE_TIMEOUT', 30, integer=True)
|
|
||||||
ACCELERATE_CONNECT_TIMEOUT = get_config(p, 'accelerate', 'accelerate_connect_timeout', 'ACCELERATE_CONNECT_TIMEOUT', 1.0, floating=True)
|
|
||||||
ACCELERATE_DAEMON_TIMEOUT = get_config(p, 'accelerate', 'accelerate_daemon_timeout', 'ACCELERATE_DAEMON_TIMEOUT', 30, integer=True)
|
|
||||||
ACCELERATE_KEYS_DIR = get_config(p, 'accelerate', 'accelerate_keys_dir', 'ACCELERATE_KEYS_DIR', '~/.fireball.keys')
|
|
||||||
ACCELERATE_KEYS_DIR_PERMS = get_config(p, 'accelerate', 'accelerate_keys_dir_perms', 'ACCELERATE_KEYS_DIR_PERMS', '700')
|
|
||||||
ACCELERATE_KEYS_FILE_PERMS = get_config(p, 'accelerate', 'accelerate_keys_file_perms', 'ACCELERATE_KEYS_FILE_PERMS', '600')
|
|
||||||
ACCELERATE_MULTI_KEY = get_config(p, 'accelerate', 'accelerate_multi_key', 'ACCELERATE_MULTI_KEY', False, boolean=True)
|
|
||||||
PARAMIKO_PTY = get_config(p, 'paramiko_connection', 'pty', 'ANSIBLE_PARAMIKO_PTY', True, boolean=True)
|
|
||||||
|
|
||||||
# characters included in auto-generated passwords
|
|
||||||
DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
|
|
||||||
|
|
||||||
# non-configurable things
|
|
||||||
DEFAULT_BECOME_PASS = None
|
|
||||||
DEFAULT_SUDO_PASS = None
|
|
||||||
DEFAULT_REMOTE_PASS = None
|
|
||||||
DEFAULT_SUBSET = None
|
|
||||||
DEFAULT_SU_PASS = None
|
|
||||||
VAULT_VERSION_MIN = 1.0
|
|
||||||
VAULT_VERSION_MAX = 1.0
|
|
@ -1,35 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
class AnsibleError(Exception):
|
|
||||||
''' The base Ansible exception from which all others should subclass '''
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AnsibleFileNotFound(AnsibleError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AnsibleConnectionFailed(AnsibleError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AnsibleYAMLValidationFailed(AnsibleError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AnsibleUndefinedVariable(AnsibleError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AnsibleFilterError(AnsibleError):
|
|
||||||
pass
|
|
@ -1,654 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
#############################################
|
|
||||||
import fnmatch
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible.inventory.ini import InventoryParser
|
|
||||||
from ansible.inventory.script import InventoryScript
|
|
||||||
from ansible.inventory.dir import InventoryDirectory
|
|
||||||
from ansible.inventory.group import Group
|
|
||||||
from ansible.inventory.host import Host
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
|
|
||||||
class Inventory(object):
|
|
||||||
"""
|
|
||||||
Host inventory for ansible.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = [ 'host_list', 'groups', '_restriction', '_also_restriction', '_subset',
|
|
||||||
'parser', '_vars_per_host', '_vars_per_group', '_hosts_cache', '_groups_list',
|
|
||||||
'_pattern_cache', '_vault_password', '_vars_plugins', '_playbook_basedir']
|
|
||||||
|
|
||||||
def __init__(self, host_list=C.DEFAULT_HOST_LIST, vault_password=None):
|
|
||||||
|
|
||||||
# the host file file, or script path, or list of hosts
|
|
||||||
# if a list, inventory data will NOT be loaded
|
|
||||||
self.host_list = host_list
|
|
||||||
self._vault_password=vault_password
|
|
||||||
|
|
||||||
# caching to avoid repeated calculations, particularly with
|
|
||||||
# external inventory scripts.
|
|
||||||
|
|
||||||
self._vars_per_host = {}
|
|
||||||
self._vars_per_group = {}
|
|
||||||
self._hosts_cache = {}
|
|
||||||
self._groups_list = {}
|
|
||||||
self._pattern_cache = {}
|
|
||||||
|
|
||||||
# to be set by calling set_playbook_basedir by playbook code
|
|
||||||
self._playbook_basedir = None
|
|
||||||
|
|
||||||
# the inventory object holds a list of groups
|
|
||||||
self.groups = []
|
|
||||||
|
|
||||||
# a list of host(names) to contain current inquiries to
|
|
||||||
self._restriction = None
|
|
||||||
self._also_restriction = None
|
|
||||||
self._subset = None
|
|
||||||
|
|
||||||
if isinstance(host_list, basestring):
|
|
||||||
if "," in host_list:
|
|
||||||
host_list = host_list.split(",")
|
|
||||||
host_list = [ h for h in host_list if h and h.strip() ]
|
|
||||||
|
|
||||||
if host_list is None:
|
|
||||||
self.parser = None
|
|
||||||
elif isinstance(host_list, list):
|
|
||||||
self.parser = None
|
|
||||||
all = Group('all')
|
|
||||||
self.groups = [ all ]
|
|
||||||
ipv6_re = re.compile('\[([a-f:A-F0-9]*[%[0-z]+]?)\](?::(\d+))?')
|
|
||||||
for x in host_list:
|
|
||||||
m = ipv6_re.match(x)
|
|
||||||
if m:
|
|
||||||
all.add_host(Host(m.groups()[0], m.groups()[1]))
|
|
||||||
else:
|
|
||||||
if ":" in x:
|
|
||||||
tokens = x.rsplit(":", 1)
|
|
||||||
# if there is ':' in the address, then this is an ipv6
|
|
||||||
if ':' in tokens[0]:
|
|
||||||
all.add_host(Host(x))
|
|
||||||
else:
|
|
||||||
all.add_host(Host(tokens[0], tokens[1]))
|
|
||||||
else:
|
|
||||||
all.add_host(Host(x))
|
|
||||||
elif os.path.exists(host_list):
|
|
||||||
if os.path.isdir(host_list):
|
|
||||||
# Ensure basedir is inside the directory
|
|
||||||
self.host_list = os.path.join(self.host_list, "")
|
|
||||||
self.parser = InventoryDirectory(filename=host_list)
|
|
||||||
self.groups = self.parser.groups.values()
|
|
||||||
else:
|
|
||||||
# check to see if the specified file starts with a
|
|
||||||
# shebang (#!/), so if an error is raised by the parser
|
|
||||||
# class we can show a more apropos error
|
|
||||||
shebang_present = False
|
|
||||||
try:
|
|
||||||
inv_file = open(host_list)
|
|
||||||
first_line = inv_file.readlines()[0]
|
|
||||||
inv_file.close()
|
|
||||||
if first_line.startswith('#!'):
|
|
||||||
shebang_present = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if utils.is_executable(host_list):
|
|
||||||
try:
|
|
||||||
self.parser = InventoryScript(filename=host_list)
|
|
||||||
self.groups = self.parser.groups.values()
|
|
||||||
except:
|
|
||||||
if not shebang_present:
|
|
||||||
raise errors.AnsibleError("The file %s is marked as executable, but failed to execute correctly. " % host_list + \
|
|
||||||
"If this is not supposed to be an executable script, correct this with `chmod -x %s`." % host_list)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.parser = InventoryParser(filename=host_list)
|
|
||||||
self.groups = self.parser.groups.values()
|
|
||||||
except:
|
|
||||||
if shebang_present:
|
|
||||||
raise errors.AnsibleError("The file %s looks like it should be an executable inventory script, but is not marked executable. " % host_list + \
|
|
||||||
"Perhaps you want to correct this with `chmod +x %s`?" % host_list)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
utils.plugins.vars_loader.add_directory(self.basedir(), with_subdir=True)
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleError("Unable to find an inventory file, specify one with -i ?")
|
|
||||||
|
|
||||||
self._vars_plugins = [ x for x in utils.plugins.vars_loader.all(self) ]
|
|
||||||
|
|
||||||
# get group vars from group_vars/ files and vars plugins
|
|
||||||
for group in self.groups:
|
|
||||||
group.vars = utils.combine_vars(group.vars, self.get_group_variables(group.name, vault_password=self._vault_password))
|
|
||||||
|
|
||||||
# get host vars from host_vars/ files and vars plugins
|
|
||||||
for host in self.get_hosts():
|
|
||||||
host.vars = utils.combine_vars(host.vars, self.get_host_variables(host.name, vault_password=self._vault_password))
|
|
||||||
|
|
||||||
|
|
||||||
def _match(self, str, pattern_str):
|
|
||||||
try:
|
|
||||||
if pattern_str.startswith('~'):
|
|
||||||
return re.search(pattern_str[1:], str)
|
|
||||||
else:
|
|
||||||
return fnmatch.fnmatch(str, pattern_str)
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError('invalid host pattern: %s' % pattern_str)
|
|
||||||
|
|
||||||
def _match_list(self, items, item_attr, pattern_str):
|
|
||||||
results = []
|
|
||||||
try:
|
|
||||||
if not pattern_str.startswith('~'):
|
|
||||||
pattern = re.compile(fnmatch.translate(pattern_str))
|
|
||||||
else:
|
|
||||||
pattern = re.compile(pattern_str[1:])
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError('invalid host pattern: %s' % pattern_str)
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
if pattern.match(getattr(item, item_attr)):
|
|
||||||
results.append(item)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def get_hosts(self, pattern="all"):
|
|
||||||
"""
|
|
||||||
find all host names matching a pattern string, taking into account any inventory restrictions or
|
|
||||||
applied subsets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# process patterns
|
|
||||||
if isinstance(pattern, list):
|
|
||||||
pattern = ';'.join(pattern)
|
|
||||||
patterns = pattern.replace(";",":").split(":")
|
|
||||||
hosts = self._get_hosts(patterns)
|
|
||||||
|
|
||||||
# exclude hosts not in a subset, if defined
|
|
||||||
if self._subset:
|
|
||||||
subset = self._get_hosts(self._subset)
|
|
||||||
hosts = [ h for h in hosts if h in subset ]
|
|
||||||
|
|
||||||
# exclude hosts mentioned in any restriction (ex: failed hosts)
|
|
||||||
if self._restriction is not None:
|
|
||||||
hosts = [ h for h in hosts if h.name in self._restriction ]
|
|
||||||
if self._also_restriction is not None:
|
|
||||||
hosts = [ h for h in hosts if h.name in self._also_restriction ]
|
|
||||||
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
def _get_hosts(self, patterns):
|
|
||||||
"""
|
|
||||||
finds hosts that match a list of patterns. Handles negative
|
|
||||||
matches as well as intersection matches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Host specifiers should be sorted to ensure consistent behavior
|
|
||||||
pattern_regular = []
|
|
||||||
pattern_intersection = []
|
|
||||||
pattern_exclude = []
|
|
||||||
for p in patterns:
|
|
||||||
if p.startswith("!"):
|
|
||||||
pattern_exclude.append(p)
|
|
||||||
elif p.startswith("&"):
|
|
||||||
pattern_intersection.append(p)
|
|
||||||
elif p:
|
|
||||||
pattern_regular.append(p)
|
|
||||||
|
|
||||||
# if no regular pattern was given, hence only exclude and/or intersection
|
|
||||||
# make that magically work
|
|
||||||
if pattern_regular == []:
|
|
||||||
pattern_regular = ['all']
|
|
||||||
|
|
||||||
# when applying the host selectors, run those without the "&" or "!"
|
|
||||||
# first, then the &s, then the !s.
|
|
||||||
patterns = pattern_regular + pattern_intersection + pattern_exclude
|
|
||||||
|
|
||||||
hosts = []
|
|
||||||
|
|
||||||
for p in patterns:
|
|
||||||
# avoid resolving a pattern that is a plain host
|
|
||||||
if p in self._hosts_cache:
|
|
||||||
hosts.append(self.get_host(p))
|
|
||||||
else:
|
|
||||||
that = self.__get_hosts(p)
|
|
||||||
if p.startswith("!"):
|
|
||||||
hosts = [ h for h in hosts if h not in that ]
|
|
||||||
elif p.startswith("&"):
|
|
||||||
hosts = [ h for h in hosts if h in that ]
|
|
||||||
else:
|
|
||||||
to_append = [ h for h in that if h.name not in [ y.name for y in hosts ] ]
|
|
||||||
hosts.extend(to_append)
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
def __get_hosts(self, pattern):
|
|
||||||
"""
|
|
||||||
finds hosts that positively match a particular pattern. Does not
|
|
||||||
take into account negative matches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if pattern in self._pattern_cache:
|
|
||||||
return self._pattern_cache[pattern]
|
|
||||||
|
|
||||||
(name, enumeration_details) = self._enumeration_info(pattern)
|
|
||||||
hpat = self._hosts_in_unenumerated_pattern(name)
|
|
||||||
result = self._apply_ranges(pattern, hpat)
|
|
||||||
self._pattern_cache[pattern] = result
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _enumeration_info(self, pattern):
|
|
||||||
"""
|
|
||||||
returns (pattern, limits) taking a regular pattern and finding out
|
|
||||||
which parts of it correspond to start/stop offsets. limits is
|
|
||||||
a tuple of (start, stop) or None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Do not parse regexes for enumeration info
|
|
||||||
if pattern.startswith('~'):
|
|
||||||
return (pattern, None)
|
|
||||||
|
|
||||||
# The regex used to match on the range, which can be [x] or [x-y].
|
|
||||||
pattern_re = re.compile("^(.*)\[([-]?[0-9]+)(?:(?:-)([0-9]+))?\](.*)$")
|
|
||||||
m = pattern_re.match(pattern)
|
|
||||||
if m:
|
|
||||||
(target, first, last, rest) = m.groups()
|
|
||||||
first = int(first)
|
|
||||||
if last:
|
|
||||||
if first < 0:
|
|
||||||
raise errors.AnsibleError("invalid range: negative indices cannot be used as the first item in a range")
|
|
||||||
last = int(last)
|
|
||||||
else:
|
|
||||||
last = first
|
|
||||||
return (target, (first, last))
|
|
||||||
else:
|
|
||||||
return (pattern, None)
|
|
||||||
|
|
||||||
def _apply_ranges(self, pat, hosts):
|
|
||||||
"""
|
|
||||||
given a pattern like foo, that matches hosts, return all of hosts
|
|
||||||
given a pattern like foo[0:5], where foo matches hosts, return the first 6 hosts
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If there are no hosts to select from, just return the
|
|
||||||
# empty set. This prevents trying to do selections on an empty set.
|
|
||||||
# issue#6258
|
|
||||||
if not hosts:
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
(loose_pattern, limits) = self._enumeration_info(pat)
|
|
||||||
if not limits:
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
(left, right) = limits
|
|
||||||
|
|
||||||
if left == '':
|
|
||||||
left = 0
|
|
||||||
if right == '':
|
|
||||||
right = 0
|
|
||||||
left=int(left)
|
|
||||||
right=int(right)
|
|
||||||
try:
|
|
||||||
if left != right:
|
|
||||||
return hosts[left:right]
|
|
||||||
else:
|
|
||||||
return [ hosts[left] ]
|
|
||||||
except IndexError:
|
|
||||||
raise errors.AnsibleError("no hosts matching the pattern '%s' were found" % pat)
|
|
||||||
|
|
||||||
def _create_implicit_localhost(self, pattern):
|
|
||||||
new_host = Host(pattern)
|
|
||||||
new_host.set_variable("ansible_python_interpreter", sys.executable)
|
|
||||||
new_host.set_variable("ansible_connection", "local")
|
|
||||||
ungrouped = self.get_group("ungrouped")
|
|
||||||
if ungrouped is None:
|
|
||||||
self.add_group(Group('ungrouped'))
|
|
||||||
ungrouped = self.get_group('ungrouped')
|
|
||||||
self.get_group('all').add_child_group(ungrouped)
|
|
||||||
ungrouped.add_host(new_host)
|
|
||||||
return new_host
|
|
||||||
|
|
||||||
def _hosts_in_unenumerated_pattern(self, pattern):
|
|
||||||
""" Get all host names matching the pattern """
|
|
||||||
|
|
||||||
results = []
|
|
||||||
hosts = []
|
|
||||||
hostnames = set()
|
|
||||||
|
|
||||||
# ignore any negative checks here, this is handled elsewhere
|
|
||||||
pattern = pattern.replace("!","").replace("&", "")
|
|
||||||
|
|
||||||
def __append_host_to_results(host):
|
|
||||||
if host not in results and host.name not in hostnames:
|
|
||||||
hostnames.add(host.name)
|
|
||||||
results.append(host)
|
|
||||||
|
|
||||||
groups = self.get_groups()
|
|
||||||
for group in groups:
|
|
||||||
if pattern == 'all':
|
|
||||||
for host in group.get_hosts():
|
|
||||||
__append_host_to_results(host)
|
|
||||||
else:
|
|
||||||
if self._match(group.name, pattern):
|
|
||||||
for host in group.get_hosts():
|
|
||||||
__append_host_to_results(host)
|
|
||||||
else:
|
|
||||||
matching_hosts = self._match_list(group.get_hosts(), 'name', pattern)
|
|
||||||
for host in matching_hosts:
|
|
||||||
__append_host_to_results(host)
|
|
||||||
|
|
||||||
if pattern in ["localhost", "127.0.0.1"] and len(results) == 0:
|
|
||||||
new_host = self._create_implicit_localhost(pattern)
|
|
||||||
results.append(new_host)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def clear_pattern_cache(self):
|
|
||||||
''' called exclusively by the add_host plugin to allow patterns to be recalculated '''
|
|
||||||
self._pattern_cache = {}
|
|
||||||
|
|
||||||
def groups_for_host(self, host):
|
|
||||||
if host in self._hosts_cache:
|
|
||||||
return self._hosts_cache[host].get_groups()
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def groups_list(self):
|
|
||||||
if not self._groups_list:
|
|
||||||
groups = {}
|
|
||||||
for g in self.groups:
|
|
||||||
groups[g.name] = [h.name for h in g.get_hosts()]
|
|
||||||
ancestors = g.get_ancestors()
|
|
||||||
for a in ancestors:
|
|
||||||
if a.name not in groups:
|
|
||||||
groups[a.name] = [h.name for h in a.get_hosts()]
|
|
||||||
self._groups_list = groups
|
|
||||||
return self._groups_list
|
|
||||||
|
|
||||||
def get_groups(self):
|
|
||||||
return self.groups
|
|
||||||
|
|
||||||
def get_host(self, hostname):
|
|
||||||
if hostname not in self._hosts_cache:
|
|
||||||
self._hosts_cache[hostname] = self._get_host(hostname)
|
|
||||||
return self._hosts_cache[hostname]
|
|
||||||
|
|
||||||
def _get_host(self, hostname):
|
|
||||||
if hostname in ['localhost','127.0.0.1']:
|
|
||||||
for host in self.get_group('all').get_hosts():
|
|
||||||
if host.name in ['localhost', '127.0.0.1']:
|
|
||||||
return host
|
|
||||||
return self._create_implicit_localhost(hostname)
|
|
||||||
else:
|
|
||||||
for group in self.groups:
|
|
||||||
for host in group.get_hosts():
|
|
||||||
if hostname == host.name:
|
|
||||||
return host
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_group(self, groupname):
|
|
||||||
for group in self.groups:
|
|
||||||
if group.name == groupname:
|
|
||||||
return group
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_group_variables(self, groupname, update_cached=False, vault_password=None):
|
|
||||||
if groupname not in self._vars_per_group or update_cached:
|
|
||||||
self._vars_per_group[groupname] = self._get_group_variables(groupname, vault_password=vault_password)
|
|
||||||
return self._vars_per_group[groupname]
|
|
||||||
|
|
||||||
def _get_group_variables(self, groupname, vault_password=None):
|
|
||||||
|
|
||||||
group = self.get_group(groupname)
|
|
||||||
if group is None:
|
|
||||||
raise errors.AnsibleError("group not found: %s" % groupname)
|
|
||||||
|
|
||||||
vars = {}
|
|
||||||
|
|
||||||
# plugin.get_group_vars retrieves just vars for specific group
|
|
||||||
vars_results = [ plugin.get_group_vars(group, vault_password=vault_password) for plugin in self._vars_plugins if hasattr(plugin, 'get_group_vars')]
|
|
||||||
for updated in vars_results:
|
|
||||||
if updated is not None:
|
|
||||||
vars = utils.combine_vars(vars, updated)
|
|
||||||
|
|
||||||
# Read group_vars/ files
|
|
||||||
vars = utils.combine_vars(vars, self.get_group_vars(group))
|
|
||||||
|
|
||||||
return vars
|
|
||||||
|
|
||||||
def get_variables(self, hostname, update_cached=False, vault_password=None):
|
|
||||||
|
|
||||||
host = self.get_host(hostname)
|
|
||||||
if not host:
|
|
||||||
raise errors.AnsibleError("host not found: %s" % hostname)
|
|
||||||
return host.get_variables()
|
|
||||||
|
|
||||||
def get_host_variables(self, hostname, update_cached=False, vault_password=None):
|
|
||||||
|
|
||||||
if hostname not in self._vars_per_host or update_cached:
|
|
||||||
self._vars_per_host[hostname] = self._get_host_variables(hostname, vault_password=vault_password)
|
|
||||||
return self._vars_per_host[hostname]
|
|
||||||
|
|
||||||
def _get_host_variables(self, hostname, vault_password=None):
|
|
||||||
|
|
||||||
host = self.get_host(hostname)
|
|
||||||
if host is None:
|
|
||||||
raise errors.AnsibleError("host not found: %s" % hostname)
|
|
||||||
|
|
||||||
vars = {}
|
|
||||||
|
|
||||||
# plugin.run retrieves all vars (also from groups) for host
|
|
||||||
vars_results = [ plugin.run(host, vault_password=vault_password) for plugin in self._vars_plugins if hasattr(plugin, 'run')]
|
|
||||||
for updated in vars_results:
|
|
||||||
if updated is not None:
|
|
||||||
vars = utils.combine_vars(vars, updated)
|
|
||||||
|
|
||||||
# plugin.get_host_vars retrieves just vars for specific host
|
|
||||||
vars_results = [ plugin.get_host_vars(host, vault_password=vault_password) for plugin in self._vars_plugins if hasattr(plugin, 'get_host_vars')]
|
|
||||||
for updated in vars_results:
|
|
||||||
if updated is not None:
|
|
||||||
vars = utils.combine_vars(vars, updated)
|
|
||||||
|
|
||||||
# still need to check InventoryParser per host vars
|
|
||||||
# which actually means InventoryScript per host,
|
|
||||||
# which is not performant
|
|
||||||
if self.parser is not None:
|
|
||||||
vars = utils.combine_vars(vars, self.parser.get_host_variables(host))
|
|
||||||
|
|
||||||
# Read host_vars/ files
|
|
||||||
vars = utils.combine_vars(vars, self.get_host_vars(host))
|
|
||||||
|
|
||||||
return vars
|
|
||||||
|
|
||||||
def add_group(self, group):
|
|
||||||
if group.name not in self.groups_list():
|
|
||||||
self.groups.append(group)
|
|
||||||
self._groups_list = None # invalidate internal cache
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleError("group already in inventory: %s" % group.name)
|
|
||||||
|
|
||||||
def list_hosts(self, pattern="all"):
|
|
||||||
|
|
||||||
""" return a list of hostnames for a pattern """
|
|
||||||
|
|
||||||
result = [ h.name for h in self.get_hosts(pattern) ]
|
|
||||||
if len(result) == 0 and pattern in ["localhost", "127.0.0.1"]:
|
|
||||||
result = [pattern]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def list_groups(self):
|
|
||||||
return sorted([ g.name for g in self.groups ], key=lambda x: x)
|
|
||||||
|
|
||||||
# TODO: remove this function
|
|
||||||
def get_restriction(self):
|
|
||||||
return self._restriction
|
|
||||||
|
|
||||||
def restrict_to(self, restriction):
|
|
||||||
"""
|
|
||||||
Restrict list operations to the hosts given in restriction. This is used
|
|
||||||
to exclude failed hosts in main playbook code, don't use this for other
|
|
||||||
reasons.
|
|
||||||
"""
|
|
||||||
if not isinstance(restriction, list):
|
|
||||||
restriction = [ restriction ]
|
|
||||||
self._restriction = restriction
|
|
||||||
|
|
||||||
def also_restrict_to(self, restriction):
|
|
||||||
"""
|
|
||||||
Works like restict_to but offers an additional restriction. Playbooks use this
|
|
||||||
to implement serial behavior.
|
|
||||||
"""
|
|
||||||
if not isinstance(restriction, list):
|
|
||||||
restriction = [ restriction ]
|
|
||||||
self._also_restriction = restriction
|
|
||||||
|
|
||||||
def subset(self, subset_pattern):
|
|
||||||
"""
|
|
||||||
Limits inventory results to a subset of inventory that matches a given
|
|
||||||
pattern, such as to select a given geographic of numeric slice amongst
|
|
||||||
a previous 'hosts' selection that only select roles, or vice versa.
|
|
||||||
Corresponds to --limit parameter to ansible-playbook
|
|
||||||
"""
|
|
||||||
if subset_pattern is None:
|
|
||||||
self._subset = None
|
|
||||||
else:
|
|
||||||
subset_pattern = subset_pattern.replace(',',':')
|
|
||||||
subset_pattern = subset_pattern.replace(";",":").split(":")
|
|
||||||
results = []
|
|
||||||
# allow Unix style @filename data
|
|
||||||
for x in subset_pattern:
|
|
||||||
if x.startswith("@"):
|
|
||||||
fd = open(x[1:])
|
|
||||||
results.extend(fd.read().split("\n"))
|
|
||||||
fd.close()
|
|
||||||
else:
|
|
||||||
results.append(x)
|
|
||||||
self._subset = results
|
|
||||||
|
|
||||||
def lift_restriction(self):
|
|
||||||
""" Do not restrict list operations """
|
|
||||||
self._restriction = None
|
|
||||||
|
|
||||||
def lift_also_restriction(self):
|
|
||||||
""" Clears the also restriction """
|
|
||||||
self._also_restriction = None
|
|
||||||
|
|
||||||
def is_file(self):
|
|
||||||
""" did inventory come from a file? """
|
|
||||||
if not isinstance(self.host_list, basestring):
|
|
||||||
return False
|
|
||||||
return os.path.exists(self.host_list)
|
|
||||||
|
|
||||||
def basedir(self):
|
|
||||||
""" if inventory came from a file, what's the directory? """
|
|
||||||
if not self.is_file():
|
|
||||||
return None
|
|
||||||
dname = os.path.dirname(self.host_list)
|
|
||||||
if dname is None or dname == '' or dname == '.':
|
|
||||||
cwd = os.getcwd()
|
|
||||||
return os.path.abspath(cwd)
|
|
||||||
return os.path.abspath(dname)
|
|
||||||
|
|
||||||
def src(self):
|
|
||||||
""" if inventory came from a file, what's the directory and file name? """
|
|
||||||
if not self.is_file():
|
|
||||||
return None
|
|
||||||
return self.host_list
|
|
||||||
|
|
||||||
def playbook_basedir(self):
|
|
||||||
""" returns the directory of the current playbook """
|
|
||||||
return self._playbook_basedir
|
|
||||||
|
|
||||||
def set_playbook_basedir(self, dir):
|
|
||||||
"""
|
|
||||||
sets the base directory of the playbook so inventory can use it as a
|
|
||||||
basedir for host_ and group_vars, and other things.
|
|
||||||
"""
|
|
||||||
# Only update things if dir is a different playbook basedir
|
|
||||||
if dir != self._playbook_basedir:
|
|
||||||
self._playbook_basedir = dir
|
|
||||||
# get group vars from group_vars/ files
|
|
||||||
for group in self.groups:
|
|
||||||
group.vars = utils.combine_vars(group.vars, self.get_group_vars(group, new_pb_basedir=True))
|
|
||||||
# get host vars from host_vars/ files
|
|
||||||
for host in self.get_hosts():
|
|
||||||
host.vars = utils.combine_vars(host.vars, self.get_host_vars(host, new_pb_basedir=True))
|
|
||||||
# invalidate cache
|
|
||||||
self._vars_per_host = {}
|
|
||||||
self._vars_per_group = {}
|
|
||||||
|
|
||||||
def get_host_vars(self, host, new_pb_basedir=False):
|
|
||||||
""" Read host_vars/ files """
|
|
||||||
return self._get_hostgroup_vars(host=host, group=None, new_pb_basedir=new_pb_basedir)
|
|
||||||
|
|
||||||
def get_group_vars(self, group, new_pb_basedir=False):
|
|
||||||
""" Read group_vars/ files """
|
|
||||||
return self._get_hostgroup_vars(host=None, group=group, new_pb_basedir=new_pb_basedir)
|
|
||||||
|
|
||||||
def _get_hostgroup_vars(self, host=None, group=None, new_pb_basedir=False):
|
|
||||||
"""
|
|
||||||
Loads variables from group_vars/<groupname> and host_vars/<hostname> in directories parallel
|
|
||||||
to the inventory base directory or in the same directory as the playbook. Variables in the playbook
|
|
||||||
dir will win over the inventory dir if files are in both.
|
|
||||||
"""
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
scan_pass = 0
|
|
||||||
_basedir = self.basedir()
|
|
||||||
|
|
||||||
# look in both the inventory base directory and the playbook base directory
|
|
||||||
# unless we do an update for a new playbook base dir
|
|
||||||
if not new_pb_basedir:
|
|
||||||
basedirs = [_basedir, self._playbook_basedir]
|
|
||||||
else:
|
|
||||||
basedirs = [self._playbook_basedir]
|
|
||||||
|
|
||||||
for basedir in basedirs:
|
|
||||||
|
|
||||||
# this can happen from particular API usages, particularly if not run
|
|
||||||
# from /usr/bin/ansible-playbook
|
|
||||||
if basedir is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
scan_pass = scan_pass + 1
|
|
||||||
|
|
||||||
# it's not an eror if the directory does not exist, keep moving
|
|
||||||
if not os.path.exists(basedir):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# save work of second scan if the directories are the same
|
|
||||||
if _basedir == self._playbook_basedir and scan_pass != 1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if group and host is None:
|
|
||||||
# load vars in dir/group_vars/name_of_group
|
|
||||||
base_path = os.path.join(basedir, "group_vars/%s" % group.name)
|
|
||||||
results = utils.load_vars(base_path, results, vault_password=self._vault_password)
|
|
||||||
|
|
||||||
elif host and group is None:
|
|
||||||
# same for hostvars in dir/host_vars/name_of_host
|
|
||||||
base_path = os.path.join(basedir, "host_vars/%s" % host.name)
|
|
||||||
results = utils.load_vars(base_path, results, vault_password=self._vault_password)
|
|
||||||
|
|
||||||
# all done, results is a dictionary of variables for this particular host.
|
|
||||||
return results
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
|||||||
# (c) 2013, Daniel Hokka Zakrisson <daniel@hozac.com>
|
|
||||||
# (c) 2014, Serge van Ginderachter <serge@vanginderachter.be>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
import os
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible.inventory.host import Host
|
|
||||||
from ansible.inventory.group import Group
|
|
||||||
from ansible.inventory.ini import InventoryParser
|
|
||||||
from ansible.inventory.script import InventoryScript
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
class InventoryDirectory(object):
|
|
||||||
''' Host inventory parser for ansible using a directory of inventories. '''
|
|
||||||
|
|
||||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
|
||||||
self.names = os.listdir(filename)
|
|
||||||
self.names.sort()
|
|
||||||
self.directory = filename
|
|
||||||
self.parsers = []
|
|
||||||
self.hosts = {}
|
|
||||||
self.groups = {}
|
|
||||||
|
|
||||||
for i in self.names:
|
|
||||||
|
|
||||||
# Skip files that end with certain extensions or characters
|
|
||||||
if any(i.endswith(ext) for ext in ("~", ".orig", ".bak", ".ini", ".retry", ".pyc", ".pyo")):
|
|
||||||
continue
|
|
||||||
# Skip hidden files
|
|
||||||
if i.startswith('.') and not i.startswith('./'):
|
|
||||||
continue
|
|
||||||
# These are things inside of an inventory basedir
|
|
||||||
if i in ("host_vars", "group_vars", "vars_plugins"):
|
|
||||||
continue
|
|
||||||
fullpath = os.path.join(self.directory, i)
|
|
||||||
if os.path.isdir(fullpath):
|
|
||||||
parser = InventoryDirectory(filename=fullpath)
|
|
||||||
elif utils.is_executable(fullpath):
|
|
||||||
parser = InventoryScript(filename=fullpath)
|
|
||||||
else:
|
|
||||||
parser = InventoryParser(filename=fullpath)
|
|
||||||
self.parsers.append(parser)
|
|
||||||
|
|
||||||
# retrieve all groups and hosts form the parser and add them to
|
|
||||||
# self, don't look at group lists yet, to avoid
|
|
||||||
# recursion trouble, but just make sure all objects exist in self
|
|
||||||
newgroups = parser.groups.values()
|
|
||||||
for group in newgroups:
|
|
||||||
for host in group.hosts:
|
|
||||||
self._add_host(host)
|
|
||||||
for group in newgroups:
|
|
||||||
self._add_group(group)
|
|
||||||
|
|
||||||
# now check the objects lists so they contain only objects from
|
|
||||||
# self; membership data in groups is already fine (except all &
|
|
||||||
# ungrouped, see later), but might still reference objects not in self
|
|
||||||
for group in self.groups.values():
|
|
||||||
# iterate on a copy of the lists, as those lists get changed in
|
|
||||||
# the loop
|
|
||||||
# list with group's child group objects:
|
|
||||||
for child in group.child_groups[:]:
|
|
||||||
if child != self.groups[child.name]:
|
|
||||||
group.child_groups.remove(child)
|
|
||||||
group.child_groups.append(self.groups[child.name])
|
|
||||||
# list with group's parent group objects:
|
|
||||||
for parent in group.parent_groups[:]:
|
|
||||||
if parent != self.groups[parent.name]:
|
|
||||||
group.parent_groups.remove(parent)
|
|
||||||
group.parent_groups.append(self.groups[parent.name])
|
|
||||||
# list with group's host objects:
|
|
||||||
for host in group.hosts[:]:
|
|
||||||
if host != self.hosts[host.name]:
|
|
||||||
group.hosts.remove(host)
|
|
||||||
group.hosts.append(self.hosts[host.name])
|
|
||||||
# also check here that the group that contains host, is
|
|
||||||
# also contained in the host's group list
|
|
||||||
if group not in self.hosts[host.name].groups:
|
|
||||||
self.hosts[host.name].groups.append(group)
|
|
||||||
|
|
||||||
# extra checks on special groups all and ungrouped
|
|
||||||
# remove hosts from 'ungrouped' if they became member of other groups
|
|
||||||
if 'ungrouped' in self.groups:
|
|
||||||
ungrouped = self.groups['ungrouped']
|
|
||||||
# loop on a copy of ungrouped hosts, as we want to change that list
|
|
||||||
for host in ungrouped.hosts[:]:
|
|
||||||
if len(host.groups) > 1:
|
|
||||||
host.groups.remove(ungrouped)
|
|
||||||
ungrouped.hosts.remove(host)
|
|
||||||
|
|
||||||
# remove hosts from 'all' if they became member of other groups
|
|
||||||
# all should only contain direct children, not grandchildren
|
|
||||||
# direct children should have dept == 1
|
|
||||||
if 'all' in self.groups:
|
|
||||||
allgroup = self.groups['all' ]
|
|
||||||
# loop on a copy of all's child groups, as we want to change that list
|
|
||||||
for group in allgroup.child_groups[:]:
|
|
||||||
# groups might once have beeen added to all, and later be added
|
|
||||||
# to another group: we need to remove the link wit all then
|
|
||||||
if len(group.parent_groups) > 1 and allgroup in group.parent_groups:
|
|
||||||
# real children of all have just 1 parent, all
|
|
||||||
# this one has more, so not a direct child of all anymore
|
|
||||||
group.parent_groups.remove(allgroup)
|
|
||||||
allgroup.child_groups.remove(group)
|
|
||||||
elif allgroup not in group.parent_groups:
|
|
||||||
# this group was once added to all, but doesn't list it as
|
|
||||||
# a parent any more; the info in the group is the correct
|
|
||||||
# info
|
|
||||||
allgroup.child_groups.remove(group)
|
|
||||||
|
|
||||||
|
|
||||||
def _add_group(self, group):
|
|
||||||
""" Merge an existing group or add a new one;
|
|
||||||
Track parent and child groups, and hosts of the new one """
|
|
||||||
|
|
||||||
if group.name not in self.groups:
|
|
||||||
# it's brand new, add him!
|
|
||||||
self.groups[group.name] = group
|
|
||||||
if self.groups[group.name] != group:
|
|
||||||
# different object, merge
|
|
||||||
self._merge_groups(self.groups[group.name], group)
|
|
||||||
|
|
||||||
def _add_host(self, host):
|
|
||||||
if host.name not in self.hosts:
|
|
||||||
# Papa's got a brand new host
|
|
||||||
self.hosts[host.name] = host
|
|
||||||
if self.hosts[host.name] != host:
|
|
||||||
# different object, merge
|
|
||||||
self._merge_hosts(self.hosts[host.name], host)
|
|
||||||
|
|
||||||
def _merge_groups(self, group, newgroup):
|
|
||||||
""" Merge all of instance newgroup into group,
|
|
||||||
update parent/child relationships
|
|
||||||
group lists may still contain group objects that exist in self with
|
|
||||||
same name, but was instanciated as a different object in some other
|
|
||||||
inventory parser; these are handled later """
|
|
||||||
|
|
||||||
# name
|
|
||||||
if group.name != newgroup.name:
|
|
||||||
raise errors.AnsibleError("Cannot merge group %s with %s" % (group.name, newgroup.name))
|
|
||||||
|
|
||||||
# depth
|
|
||||||
group.depth = max([group.depth, newgroup.depth])
|
|
||||||
|
|
||||||
# hosts list (host objects are by now already added to self.hosts)
|
|
||||||
for host in newgroup.hosts:
|
|
||||||
grouphosts = dict([(h.name, h) for h in group.hosts])
|
|
||||||
if host.name in grouphosts:
|
|
||||||
# same host name but different object, merge
|
|
||||||
self._merge_hosts(grouphosts[host.name], host)
|
|
||||||
else:
|
|
||||||
# new membership, add host to group from self
|
|
||||||
# group from self will also be added again to host.groups, but
|
|
||||||
# as different object
|
|
||||||
group.add_host(self.hosts[host.name])
|
|
||||||
# now remove this the old object for group in host.groups
|
|
||||||
for hostgroup in [g for g in host.groups]:
|
|
||||||
if hostgroup.name == group.name and hostgroup != self.groups[group.name]:
|
|
||||||
self.hosts[host.name].groups.remove(hostgroup)
|
|
||||||
|
|
||||||
|
|
||||||
# group child membership relation
|
|
||||||
for newchild in newgroup.child_groups:
|
|
||||||
# dict with existing child groups:
|
|
||||||
childgroups = dict([(g.name, g) for g in group.child_groups])
|
|
||||||
# check if child of new group is already known as a child
|
|
||||||
if newchild.name not in childgroups:
|
|
||||||
self.groups[group.name].add_child_group(newchild)
|
|
||||||
|
|
||||||
# group parent membership relation
|
|
||||||
for newparent in newgroup.parent_groups:
|
|
||||||
# dict with existing parent groups:
|
|
||||||
parentgroups = dict([(g.name, g) for g in group.parent_groups])
|
|
||||||
# check if parent of new group is already known as a parent
|
|
||||||
if newparent.name not in parentgroups:
|
|
||||||
if newparent.name not in self.groups:
|
|
||||||
# group does not exist yet in self, import him
|
|
||||||
self.groups[newparent.name] = newparent
|
|
||||||
# group now exists but not yet as a parent here
|
|
||||||
self.groups[newparent.name].add_child_group(group)
|
|
||||||
|
|
||||||
# variables
|
|
||||||
group.vars = utils.combine_vars(group.vars, newgroup.vars)
|
|
||||||
|
|
||||||
def _merge_hosts(self,host, newhost):
|
|
||||||
""" Merge all of instance newhost into host """
|
|
||||||
|
|
||||||
# name
|
|
||||||
if host.name != newhost.name:
|
|
||||||
raise errors.AnsibleError("Cannot merge host %s with %s" % (host.name, newhost.name))
|
|
||||||
|
|
||||||
# group membership relation
|
|
||||||
for newgroup in newhost.groups:
|
|
||||||
# dict with existing groups:
|
|
||||||
hostgroups = dict([(g.name, g) for g in host.groups])
|
|
||||||
# check if new group is already known as a group
|
|
||||||
if newgroup.name not in hostgroups:
|
|
||||||
if newgroup.name not in self.groups:
|
|
||||||
# group does not exist yet in self, import him
|
|
||||||
self.groups[newgroup.name] = newgroup
|
|
||||||
# group now exists but doesn't have host yet
|
|
||||||
self.groups[newgroup.name].add_host(host)
|
|
||||||
|
|
||||||
# variables
|
|
||||||
host.vars = utils.combine_vars(host.vars, newhost.vars)
|
|
||||||
|
|
||||||
def get_host_variables(self, host):
|
|
||||||
""" Gets additional host variables from all inventories """
|
|
||||||
vars = {}
|
|
||||||
for i in self.parsers:
|
|
||||||
vars.update(i.get_host_variables(host))
|
|
||||||
return vars
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
|||||||
# (c) 2012, Zettar Inc.
|
|
||||||
# Written by Chin Fang <fangchin@zettar.com>
|
|
||||||
#
|
|
||||||
# This file is part of Ansible
|
|
||||||
#
|
|
||||||
# This module 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.
|
|
||||||
#
|
|
||||||
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
'''
|
|
||||||
This module is for enhancing ansible's inventory parsing capability such
|
|
||||||
that it can deal with hostnames specified using a simple pattern in the
|
|
||||||
form of [beg:end], example: [1:5], [a:c], [D:G]. If beg is not specified,
|
|
||||||
it defaults to 0.
|
|
||||||
|
|
||||||
If beg is given and is left-zero-padded, e.g. '001', it is taken as a
|
|
||||||
formatting hint when the range is expanded. e.g. [001:010] is to be
|
|
||||||
expanded into 001, 002 ...009, 010.
|
|
||||||
|
|
||||||
Note that when beg is specified with left zero padding, then the length of
|
|
||||||
end must be the same as that of beg, else an exception is raised.
|
|
||||||
'''
|
|
||||||
import string
|
|
||||||
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
def detect_range(line = None):
|
|
||||||
'''
|
|
||||||
A helper function that checks a given host line to see if it contains
|
|
||||||
a range pattern described in the docstring above.
|
|
||||||
|
|
||||||
Returnes True if the given line contains a pattern, else False.
|
|
||||||
'''
|
|
||||||
if 0 <= line.find("[") < line.find(":") < line.find("]"):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def expand_hostname_range(line = None):
|
|
||||||
'''
|
|
||||||
A helper function that expands a given line that contains a pattern
|
|
||||||
specified in top docstring, and returns a list that consists of the
|
|
||||||
expanded version.
|
|
||||||
|
|
||||||
The '[' and ']' characters are used to maintain the pseudo-code
|
|
||||||
appearance. They are replaced in this function with '|' to ease
|
|
||||||
string splitting.
|
|
||||||
|
|
||||||
References: http://ansible.github.com/patterns.html#hosts-and-groups
|
|
||||||
'''
|
|
||||||
all_hosts = []
|
|
||||||
if line:
|
|
||||||
# A hostname such as db[1:6]-node is considered to consists
|
|
||||||
# three parts:
|
|
||||||
# head: 'db'
|
|
||||||
# nrange: [1:6]; range() is a built-in. Can't use the name
|
|
||||||
# tail: '-node'
|
|
||||||
|
|
||||||
# Add support for multiple ranges in a host so:
|
|
||||||
# db[01:10:3]node-[01:10]
|
|
||||||
# - to do this we split off at the first [...] set, getting the list
|
|
||||||
# of hosts and then repeat until none left.
|
|
||||||
# - also add an optional third parameter which contains the step. (Default: 1)
|
|
||||||
# so range can be [01:10:2] -> 01 03 05 07 09
|
|
||||||
# FIXME: make this work for alphabetic sequences too.
|
|
||||||
|
|
||||||
(head, nrange, tail) = line.replace('[','|',1).replace(']','|',1).split('|')
|
|
||||||
bounds = nrange.split(":")
|
|
||||||
if len(bounds) != 2 and len(bounds) != 3:
|
|
||||||
raise errors.AnsibleError("host range incorrectly specified")
|
|
||||||
beg = bounds[0]
|
|
||||||
end = bounds[1]
|
|
||||||
if len(bounds) == 2:
|
|
||||||
step = 1
|
|
||||||
else:
|
|
||||||
step = bounds[2]
|
|
||||||
if not beg:
|
|
||||||
beg = "0"
|
|
||||||
if not end:
|
|
||||||
raise errors.AnsibleError("host range end value missing")
|
|
||||||
if beg[0] == '0' and len(beg) > 1:
|
|
||||||
rlen = len(beg) # range length formatting hint
|
|
||||||
if rlen != len(end):
|
|
||||||
raise errors.AnsibleError("host range format incorrectly specified!")
|
|
||||||
fill = lambda _: str(_).zfill(rlen) # range sequence
|
|
||||||
else:
|
|
||||||
fill = str
|
|
||||||
|
|
||||||
try:
|
|
||||||
i_beg = string.ascii_letters.index(beg)
|
|
||||||
i_end = string.ascii_letters.index(end)
|
|
||||||
if i_beg > i_end:
|
|
||||||
raise errors.AnsibleError("host range format incorrectly specified!")
|
|
||||||
seq = string.ascii_letters[i_beg:i_end+1]
|
|
||||||
except ValueError: # not an alpha range
|
|
||||||
seq = range(int(beg), int(end)+1, int(step))
|
|
||||||
|
|
||||||
for rseq in seq:
|
|
||||||
hname = ''.join((head, fill(rseq), tail))
|
|
||||||
|
|
||||||
if detect_range(hname):
|
|
||||||
all_hosts.extend( expand_hostname_range( hname ) )
|
|
||||||
else:
|
|
||||||
all_hosts.append(hname)
|
|
||||||
|
|
||||||
return all_hosts
|
|
@ -1,117 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
class Group(object):
|
|
||||||
''' a group of ansible hosts '''
|
|
||||||
|
|
||||||
__slots__ = [ 'name', 'hosts', 'vars', 'child_groups', 'parent_groups', 'depth', '_hosts_cache' ]
|
|
||||||
|
|
||||||
def __init__(self, name=None):
|
|
||||||
|
|
||||||
self.depth = 0
|
|
||||||
self.name = name
|
|
||||||
self.hosts = []
|
|
||||||
self.vars = {}
|
|
||||||
self.child_groups = []
|
|
||||||
self.parent_groups = []
|
|
||||||
self._hosts_cache = None
|
|
||||||
#self.clear_hosts_cache()
|
|
||||||
if self.name is None:
|
|
||||||
raise Exception("group name is required")
|
|
||||||
|
|
||||||
def add_child_group(self, group):
|
|
||||||
|
|
||||||
if self == group:
|
|
||||||
raise Exception("can't add group to itself")
|
|
||||||
|
|
||||||
# don't add if it's already there
|
|
||||||
if not group in self.child_groups:
|
|
||||||
self.child_groups.append(group)
|
|
||||||
|
|
||||||
# update the depth of the child
|
|
||||||
group.depth = max([self.depth+1, group.depth])
|
|
||||||
|
|
||||||
# update the depth of the grandchildren
|
|
||||||
group._check_children_depth()
|
|
||||||
|
|
||||||
# now add self to child's parent_groups list, but only if there
|
|
||||||
# isn't already a group with the same name
|
|
||||||
if not self.name in [g.name for g in group.parent_groups]:
|
|
||||||
group.parent_groups.append(self)
|
|
||||||
|
|
||||||
self.clear_hosts_cache()
|
|
||||||
|
|
||||||
def _check_children_depth(self):
|
|
||||||
|
|
||||||
for group in self.child_groups:
|
|
||||||
group.depth = max([self.depth+1, group.depth])
|
|
||||||
group._check_children_depth()
|
|
||||||
|
|
||||||
def add_host(self, host):
|
|
||||||
|
|
||||||
self.hosts.append(host)
|
|
||||||
host.add_group(self)
|
|
||||||
self.clear_hosts_cache()
|
|
||||||
|
|
||||||
def set_variable(self, key, value):
|
|
||||||
|
|
||||||
self.vars[key] = value
|
|
||||||
|
|
||||||
def clear_hosts_cache(self):
|
|
||||||
|
|
||||||
self._hosts_cache = None
|
|
||||||
for g in self.parent_groups:
|
|
||||||
g.clear_hosts_cache()
|
|
||||||
|
|
||||||
def get_hosts(self):
|
|
||||||
|
|
||||||
if self._hosts_cache is None:
|
|
||||||
self._hosts_cache = self._get_hosts()
|
|
||||||
|
|
||||||
return self._hosts_cache
|
|
||||||
|
|
||||||
def _get_hosts(self):
|
|
||||||
|
|
||||||
hosts = []
|
|
||||||
seen = {}
|
|
||||||
for kid in self.child_groups:
|
|
||||||
kid_hosts = kid.get_hosts()
|
|
||||||
for kk in kid_hosts:
|
|
||||||
if kk not in seen:
|
|
||||||
seen[kk] = 1
|
|
||||||
hosts.append(kk)
|
|
||||||
for mine in self.hosts:
|
|
||||||
if mine not in seen:
|
|
||||||
seen[mine] = 1
|
|
||||||
hosts.append(mine)
|
|
||||||
return hosts
|
|
||||||
|
|
||||||
def get_variables(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()
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible import utils
|
|
||||||
|
|
||||||
class Host(object):
|
|
||||||
''' a single ansible host '''
|
|
||||||
|
|
||||||
__slots__ = [ 'name', 'vars', 'groups' ]
|
|
||||||
|
|
||||||
def __init__(self, name=None, port=None):
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.vars = {}
|
|
||||||
self.groups = []
|
|
||||||
if port and port != C.DEFAULT_REMOTE_PORT:
|
|
||||||
self.set_variable('ansible_ssh_port', int(port))
|
|
||||||
|
|
||||||
if self.name is None:
|
|
||||||
raise Exception("host name is required")
|
|
||||||
|
|
||||||
def add_group(self, group):
|
|
||||||
|
|
||||||
self.groups.append(group)
|
|
||||||
|
|
||||||
def set_variable(self, key, value):
|
|
||||||
|
|
||||||
self.vars[key]=value
|
|
||||||
|
|
||||||
def get_groups(self):
|
|
||||||
|
|
||||||
groups = {}
|
|
||||||
for g in self.groups:
|
|
||||||
groups[g.name] = g
|
|
||||||
ancestors = g.get_ancestors()
|
|
||||||
for a in ancestors:
|
|
||||||
groups[a.name] = a
|
|
||||||
return groups.values()
|
|
||||||
|
|
||||||
def get_variables(self):
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
groups = self.get_groups()
|
|
||||||
for group in sorted(groups, key=lambda g: g.depth):
|
|
||||||
results = utils.combine_vars(results, group.get_variables())
|
|
||||||
results = utils.combine_vars(results, self.vars)
|
|
||||||
results['inventory_hostname'] = self.name
|
|
||||||
results['inventory_hostname_short'] = self.name.split('.')[0]
|
|
||||||
results['group_names'] = sorted([ g.name for g in groups if g.name != 'all'])
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible.inventory.host import Host
|
|
||||||
from ansible.inventory.group import Group
|
|
||||||
from ansible.inventory.expand_hosts import detect_range
|
|
||||||
from ansible.inventory.expand_hosts import expand_hostname_range
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
import shlex
|
|
||||||
import re
|
|
||||||
import ast
|
|
||||||
|
|
||||||
class InventoryParser(object):
|
|
||||||
"""
|
|
||||||
Host inventory for ansible.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
|
||||||
|
|
||||||
with open(filename) as fh:
|
|
||||||
self.filename = filename
|
|
||||||
self.lines = fh.readlines()
|
|
||||||
self.groups = {}
|
|
||||||
self.hosts = {}
|
|
||||||
self._parse()
|
|
||||||
|
|
||||||
def _parse(self):
|
|
||||||
|
|
||||||
self._parse_base_groups()
|
|
||||||
self._parse_group_children()
|
|
||||||
self._add_allgroup_children()
|
|
||||||
self._parse_group_variables()
|
|
||||||
return self.groups
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_value(v):
|
|
||||||
if "#" not in v:
|
|
||||||
try:
|
|
||||||
ret = ast.literal_eval(v)
|
|
||||||
if not isinstance(ret, float):
|
|
||||||
# Do not trim floats. Eg: "1.20" to 1.2
|
|
||||||
return ret
|
|
||||||
# Using explicit exceptions.
|
|
||||||
# Likely a string that literal_eval does not like. We wil then just set it.
|
|
||||||
except ValueError:
|
|
||||||
# For some reason this was thought to be malformed.
|
|
||||||
pass
|
|
||||||
except SyntaxError:
|
|
||||||
# Is this a hash with an equals at the end?
|
|
||||||
pass
|
|
||||||
return v
|
|
||||||
|
|
||||||
# [webservers]
|
|
||||||
# alpha
|
|
||||||
# beta:2345
|
|
||||||
# gamma sudo=True user=root
|
|
||||||
# delta asdf=jkl favcolor=red
|
|
||||||
|
|
||||||
def _add_allgroup_children(self):
|
|
||||||
|
|
||||||
for group in self.groups.values():
|
|
||||||
if group.depth == 0 and group.name != 'all':
|
|
||||||
self.groups['all'].add_child_group(group)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_base_groups(self):
|
|
||||||
# FIXME: refactor
|
|
||||||
|
|
||||||
ungrouped = Group(name='ungrouped')
|
|
||||||
all = Group(name='all')
|
|
||||||
all.add_child_group(ungrouped)
|
|
||||||
|
|
||||||
self.groups = dict(all=all, ungrouped=ungrouped)
|
|
||||||
active_group_name = 'ungrouped'
|
|
||||||
|
|
||||||
for lineno in range(len(self.lines)):
|
|
||||||
line = utils.before_comment(self.lines[lineno]).strip()
|
|
||||||
if line.startswith("[") and line.endswith("]"):
|
|
||||||
active_group_name = line.replace("[","").replace("]","")
|
|
||||||
if ":vars" in line or ":children" in line:
|
|
||||||
active_group_name = active_group_name.rsplit(":", 1)[0]
|
|
||||||
if active_group_name not in self.groups:
|
|
||||||
new_group = self.groups[active_group_name] = Group(name=active_group_name)
|
|
||||||
active_group_name = None
|
|
||||||
elif active_group_name not in self.groups:
|
|
||||||
new_group = self.groups[active_group_name] = Group(name=active_group_name)
|
|
||||||
elif line.startswith(";") or line == '':
|
|
||||||
pass
|
|
||||||
elif active_group_name:
|
|
||||||
tokens = shlex.split(line)
|
|
||||||
if len(tokens) == 0:
|
|
||||||
continue
|
|
||||||
hostname = tokens[0]
|
|
||||||
port = C.DEFAULT_REMOTE_PORT
|
|
||||||
# Three cases to check:
|
|
||||||
# 0. A hostname that contains a range pesudo-code and a port
|
|
||||||
# 1. A hostname that contains just a port
|
|
||||||
if hostname.count(":") > 1:
|
|
||||||
# Possible an IPv6 address, or maybe a host line with multiple ranges
|
|
||||||
# IPv6 with Port XXX:XXX::XXX.port
|
|
||||||
# FQDN foo.example.com
|
|
||||||
if hostname.count(".") == 1:
|
|
||||||
(hostname, port) = hostname.rsplit(".", 1)
|
|
||||||
elif ("[" in hostname and
|
|
||||||
"]" in hostname and
|
|
||||||
":" in hostname and
|
|
||||||
(hostname.rindex("]") < hostname.rindex(":")) or
|
|
||||||
("]" not in hostname and ":" in hostname)):
|
|
||||||
(hostname, port) = hostname.rsplit(":", 1)
|
|
||||||
|
|
||||||
hostnames = []
|
|
||||||
if detect_range(hostname):
|
|
||||||
hostnames = expand_hostname_range(hostname)
|
|
||||||
else:
|
|
||||||
hostnames = [hostname]
|
|
||||||
|
|
||||||
for hn in hostnames:
|
|
||||||
host = None
|
|
||||||
if hn in self.hosts:
|
|
||||||
host = self.hosts[hn]
|
|
||||||
else:
|
|
||||||
host = Host(name=hn, port=port)
|
|
||||||
self.hosts[hn] = host
|
|
||||||
if len(tokens) > 1:
|
|
||||||
for t in tokens[1:]:
|
|
||||||
if t.startswith('#'):
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
(k,v) = t.split("=", 1)
|
|
||||||
except ValueError, e:
|
|
||||||
raise errors.AnsibleError("%s:%s: Invalid ini entry: %s - %s" % (self.filename, lineno + 1, t, str(e)))
|
|
||||||
host.set_variable(k, self._parse_value(v))
|
|
||||||
self.groups[active_group_name].add_host(host)
|
|
||||||
|
|
||||||
# [southeast:children]
|
|
||||||
# atlanta
|
|
||||||
# raleigh
|
|
||||||
|
|
||||||
def _parse_group_children(self):
|
|
||||||
group = None
|
|
||||||
|
|
||||||
for lineno in range(len(self.lines)):
|
|
||||||
line = self.lines[lineno].strip()
|
|
||||||
if line is None or line == '':
|
|
||||||
continue
|
|
||||||
if line.startswith("[") and ":children]" in line:
|
|
||||||
line = line.replace("[","").replace(":children]","")
|
|
||||||
group = self.groups.get(line, None)
|
|
||||||
if group is None:
|
|
||||||
group = self.groups[line] = Group(name=line)
|
|
||||||
elif line.startswith("#") or line.startswith(";"):
|
|
||||||
pass
|
|
||||||
elif line.startswith("["):
|
|
||||||
group = None
|
|
||||||
elif group:
|
|
||||||
kid_group = self.groups.get(line, None)
|
|
||||||
if kid_group is None:
|
|
||||||
raise errors.AnsibleError("%s:%d: child group is not defined: (%s)" % (self.filename, lineno + 1, line))
|
|
||||||
else:
|
|
||||||
group.add_child_group(kid_group)
|
|
||||||
|
|
||||||
|
|
||||||
# [webservers:vars]
|
|
||||||
# http_port=1234
|
|
||||||
# maxRequestsPerChild=200
|
|
||||||
|
|
||||||
def _parse_group_variables(self):
|
|
||||||
group = None
|
|
||||||
for lineno in range(len(self.lines)):
|
|
||||||
line = self.lines[lineno].strip()
|
|
||||||
if line.startswith("[") and ":vars]" in line:
|
|
||||||
line = line.replace("[","").replace(":vars]","")
|
|
||||||
group = self.groups.get(line, None)
|
|
||||||
if group is None:
|
|
||||||
raise errors.AnsibleError("%s:%d: can't add vars to undefined group: %s" % (self.filename, lineno + 1, line))
|
|
||||||
elif line.startswith("#") or line.startswith(";"):
|
|
||||||
pass
|
|
||||||
elif line.startswith("["):
|
|
||||||
group = None
|
|
||||||
elif line == '':
|
|
||||||
pass
|
|
||||||
elif group:
|
|
||||||
if "=" not in line:
|
|
||||||
raise errors.AnsibleError("%s:%d: variables assigned to group must be in key=value form" % (self.filename, lineno + 1))
|
|
||||||
else:
|
|
||||||
(k, v) = [e.strip() for e in line.split("=", 1)]
|
|
||||||
group.set_variable(k, self._parse_value(v))
|
|
||||||
|
|
||||||
def get_host_variables(self, host):
|
|
||||||
return {}
|
|
@ -1,154 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible.inventory.host import Host
|
|
||||||
from ansible.inventory.group import Group
|
|
||||||
from ansible.module_utils.basic import json_dict_bytes_to_unicode
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryScript(object):
|
|
||||||
''' Host inventory parser for ansible using external inventory scripts. '''
|
|
||||||
|
|
||||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
|
||||||
|
|
||||||
# Support inventory scripts that are not prefixed with some
|
|
||||||
# path information but happen to be in the current working
|
|
||||||
# directory when '.' is not in PATH.
|
|
||||||
self.filename = os.path.abspath(filename)
|
|
||||||
cmd = [ self.filename, "--list" ]
|
|
||||||
try:
|
|
||||||
sp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
except OSError, e:
|
|
||||||
raise errors.AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
|
|
||||||
(stdout, stderr) = sp.communicate()
|
|
||||||
|
|
||||||
if sp.returncode != 0:
|
|
||||||
raise errors.AnsibleError("Inventory script (%s) had an execution error: %s " % (filename,stderr))
|
|
||||||
|
|
||||||
self.data = stdout
|
|
||||||
# see comment about _meta below
|
|
||||||
self.host_vars_from_top = None
|
|
||||||
self.groups = self._parse(stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse(self, err):
|
|
||||||
|
|
||||||
all_hosts = {}
|
|
||||||
|
|
||||||
# not passing from_remote because data from CMDB is trusted
|
|
||||||
self.raw = utils.parse_json(self.data)
|
|
||||||
self.raw = json_dict_bytes_to_unicode(self.raw)
|
|
||||||
|
|
||||||
all = Group('all')
|
|
||||||
groups = dict(all=all)
|
|
||||||
group = None
|
|
||||||
|
|
||||||
|
|
||||||
if 'failed' in self.raw:
|
|
||||||
sys.stderr.write(err + "\n")
|
|
||||||
raise errors.AnsibleError("failed to parse executable inventory script results: %s" % self.raw)
|
|
||||||
|
|
||||||
for (group_name, data) in self.raw.items():
|
|
||||||
|
|
||||||
# in Ansible 1.3 and later, a "_meta" subelement may contain
|
|
||||||
# a variable "hostvars" which contains a hash for each host
|
|
||||||
# if this "hostvars" exists at all then do not call --host for each
|
|
||||||
# host. This is for efficiency and scripts should still return data
|
|
||||||
# if called with --host for backwards compat with 1.2 and earlier.
|
|
||||||
|
|
||||||
if group_name == '_meta':
|
|
||||||
if 'hostvars' in data:
|
|
||||||
self.host_vars_from_top = data['hostvars']
|
|
||||||
continue
|
|
||||||
|
|
||||||
if group_name != all.name:
|
|
||||||
group = groups[group_name] = Group(group_name)
|
|
||||||
else:
|
|
||||||
group = all
|
|
||||||
host = None
|
|
||||||
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
data = {'hosts': data}
|
|
||||||
# is not those subkeys, then simplified syntax, host with vars
|
|
||||||
elif not any(k in data for k in ('hosts','vars','children')):
|
|
||||||
data = {'hosts': [group_name], 'vars': data}
|
|
||||||
|
|
||||||
if 'hosts' in data:
|
|
||||||
if not isinstance(data['hosts'], list):
|
|
||||||
raise errors.AnsibleError("You defined a group \"%s\" with bad "
|
|
||||||
"data for the host list:\n %s" % (group_name, data))
|
|
||||||
|
|
||||||
for hostname in data['hosts']:
|
|
||||||
if not hostname in all_hosts:
|
|
||||||
all_hosts[hostname] = Host(hostname)
|
|
||||||
host = all_hosts[hostname]
|
|
||||||
group.add_host(host)
|
|
||||||
|
|
||||||
if 'vars' in data:
|
|
||||||
if not isinstance(data['vars'], dict):
|
|
||||||
raise errors.AnsibleError("You defined a group \"%s\" with bad "
|
|
||||||
"data for variables:\n %s" % (group_name, data))
|
|
||||||
|
|
||||||
for k, v in data['vars'].iteritems():
|
|
||||||
if group.name == all.name:
|
|
||||||
all.set_variable(k, v)
|
|
||||||
else:
|
|
||||||
group.set_variable(k, v)
|
|
||||||
|
|
||||||
# Separate loop to ensure all groups are defined
|
|
||||||
for (group_name, data) in self.raw.items():
|
|
||||||
if group_name == '_meta':
|
|
||||||
continue
|
|
||||||
if isinstance(data, dict) and 'children' in data:
|
|
||||||
for child_name in data['children']:
|
|
||||||
if child_name in groups:
|
|
||||||
groups[group_name].add_child_group(groups[child_name])
|
|
||||||
|
|
||||||
for group in groups.values():
|
|
||||||
if group.depth == 0 and group.name != 'all':
|
|
||||||
all.add_child_group(group)
|
|
||||||
|
|
||||||
return groups
|
|
||||||
|
|
||||||
def get_host_variables(self, host):
|
|
||||||
""" Runs <script> --host <hostname> to determine additional host variables """
|
|
||||||
if self.host_vars_from_top is not None:
|
|
||||||
got = self.host_vars_from_top.get(host.name, {})
|
|
||||||
return got
|
|
||||||
|
|
||||||
|
|
||||||
cmd = [self.filename, "--host", host.name]
|
|
||||||
try:
|
|
||||||
sp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
except OSError, e:
|
|
||||||
raise errors.AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
|
|
||||||
(out, err) = sp.communicate()
|
|
||||||
if out.strip() == '':
|
|
||||||
return dict()
|
|
||||||
try:
|
|
||||||
return json_dict_bytes_to_unicode(utils.parse_json(out))
|
|
||||||
except ValueError:
|
|
||||||
raise errors.AnsibleError("could not parse post variable response: %s, %s" % (cmd, out))
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# (c) 2014, Serge van Ginderachter <serge@vanginderachter.be>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
class VarsModule(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Loads variables for groups and/or hosts
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, inventory):
|
|
||||||
|
|
||||||
""" constructor """
|
|
||||||
|
|
||||||
self.inventory = inventory
|
|
||||||
self.inventory_basedir = inventory.basedir()
|
|
||||||
|
|
||||||
|
|
||||||
def run(self, host, vault_password=None):
|
|
||||||
""" For backwards compatibility, when only vars per host were retrieved
|
|
||||||
This method should return both host specific vars as well as vars
|
|
||||||
calculated from groups it is a member of """
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_host_vars(self, host, vault_password=None):
|
|
||||||
""" Get host specific variables. """
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_group_vars(self, group, vault_password=None):
|
|
||||||
""" Get group specific variables. """
|
|
||||||
return {}
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
|||||||
# (c) 2013-2014, Michael DeHaan <michael.dehaan@gmail.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 python and deps
|
|
||||||
from cStringIO import StringIO
|
|
||||||
import inspect
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
# from Ansible
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import constants as C
|
|
||||||
from ansible import __version__
|
|
||||||
from ansible.utils.unicode import to_bytes
|
|
||||||
|
|
||||||
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
|
|
||||||
REPLACER_ARGS = "\"<<INCLUDE_ANSIBLE_MODULE_ARGS>>\""
|
|
||||||
REPLACER_COMPLEX = "\"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>\""
|
|
||||||
REPLACER_WINDOWS = "# POWERSHELL_COMMON"
|
|
||||||
REPLACER_VERSION = "\"<<ANSIBLE_VERSION>>\""
|
|
||||||
REPLACER_SELINUX = "<<SELINUX_SPECIAL_FILESYSTEMS>>"
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleReplacer(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
The Replacer is used to insert chunks of code into modules before
|
|
||||||
transfer. Rather than doing classical python imports, this allows for more
|
|
||||||
efficient transfer in a no-bootstrapping scenario by not moving extra files
|
|
||||||
over the wire, and also takes care of embedding arguments in the transferred
|
|
||||||
modules.
|
|
||||||
|
|
||||||
This version is done in such a way that local imports can still be
|
|
||||||
used in the module code, so IDEs don't have to be aware of what is going on.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import *
|
|
||||||
|
|
||||||
... will result in the insertion basic.py into the module
|
|
||||||
|
|
||||||
from the module_utils/ directory in the source tree.
|
|
||||||
|
|
||||||
All modules are required to import at least basic, though there will also
|
|
||||||
be other snippets.
|
|
||||||
|
|
||||||
# POWERSHELL_COMMON
|
|
||||||
|
|
||||||
Also results in the inclusion of the common code in powershell.ps1
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ******************************************************************************
|
|
||||||
|
|
||||||
def __init__(self, strip_comments=False):
|
|
||||||
this_file = inspect.getfile(inspect.currentframe())
|
|
||||||
self.snippet_path = os.path.join(os.path.dirname(this_file), 'module_utils')
|
|
||||||
self.strip_comments = strip_comments # TODO: implement
|
|
||||||
|
|
||||||
# ******************************************************************************
|
|
||||||
|
|
||||||
|
|
||||||
def slurp(self, path):
|
|
||||||
if not os.path.exists(path):
|
|
||||||
raise errors.AnsibleError("imported module support code does not exist at %s" % path)
|
|
||||||
fd = open(path)
|
|
||||||
data = fd.read()
|
|
||||||
fd.close()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _find_snippet_imports(self, module_data, module_path):
|
|
||||||
"""
|
|
||||||
Given the source of the module, convert it to a Jinja2 template to insert
|
|
||||||
module code and return whether it's a new or old style module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
module_style = 'old'
|
|
||||||
if REPLACER in module_data:
|
|
||||||
module_style = 'new'
|
|
||||||
elif 'from ansible.module_utils.' in module_data:
|
|
||||||
module_style = 'new'
|
|
||||||
elif 'WANT_JSON' in module_data:
|
|
||||||
module_style = 'non_native_want_json'
|
|
||||||
|
|
||||||
output = StringIO()
|
|
||||||
lines = module_data.split('\n')
|
|
||||||
snippet_names = []
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
|
|
||||||
if REPLACER in line:
|
|
||||||
output.write(self.slurp(os.path.join(self.snippet_path, "basic.py")))
|
|
||||||
snippet_names.append('basic')
|
|
||||||
if REPLACER_WINDOWS in line:
|
|
||||||
ps_data = self.slurp(os.path.join(self.snippet_path, "powershell.ps1"))
|
|
||||||
output.write(ps_data)
|
|
||||||
snippet_names.append('powershell')
|
|
||||||
elif line.startswith('from ansible.module_utils.'):
|
|
||||||
tokens=line.split(".")
|
|
||||||
import_error = False
|
|
||||||
if len(tokens) != 3:
|
|
||||||
import_error = True
|
|
||||||
if " import *" not in line:
|
|
||||||
import_error = True
|
|
||||||
if import_error:
|
|
||||||
raise errors.AnsibleError("error importing module in %s, expecting format like 'from ansible.module_utils.basic import *'" % module_path)
|
|
||||||
snippet_name = tokens[2].split()[0]
|
|
||||||
snippet_names.append(snippet_name)
|
|
||||||
output.write(self.slurp(os.path.join(self.snippet_path, snippet_name + ".py")))
|
|
||||||
|
|
||||||
else:
|
|
||||||
if self.strip_comments and line.startswith("#") or line == '':
|
|
||||||
pass
|
|
||||||
output.write(line)
|
|
||||||
output.write("\n")
|
|
||||||
|
|
||||||
if not module_path.endswith(".ps1"):
|
|
||||||
# Unixy modules
|
|
||||||
if len(snippet_names) > 0 and not 'basic' in snippet_names:
|
|
||||||
raise errors.AnsibleError("missing required import in %s: from ansible.module_utils.basic import *" % module_path)
|
|
||||||
else:
|
|
||||||
# Windows modules
|
|
||||||
if len(snippet_names) > 0 and not 'powershell' in snippet_names:
|
|
||||||
raise errors.AnsibleError("missing required import in %s: # POWERSHELL_COMMON" % module_path)
|
|
||||||
|
|
||||||
return (output.getvalue(), module_style)
|
|
||||||
|
|
||||||
# ******************************************************************************
|
|
||||||
|
|
||||||
def modify_module(self, module_path, complex_args, module_args, inject):
|
|
||||||
|
|
||||||
with open(module_path) as f:
|
|
||||||
|
|
||||||
# read in the module source
|
|
||||||
module_data = f.read()
|
|
||||||
|
|
||||||
(module_data, module_style) = self._find_snippet_imports(module_data, module_path)
|
|
||||||
|
|
||||||
complex_args_json = utils.jsonify(complex_args)
|
|
||||||
# We force conversion of module_args to str because module_common calls shlex.split,
|
|
||||||
# a standard library function that incorrectly handles Unicode input before Python 2.7.3.
|
|
||||||
# Note: it would be better to do all this conversion at the border
|
|
||||||
# (when the data is originally parsed into data structures) but
|
|
||||||
# it's currently coming from too many sources to make that
|
|
||||||
# effective.
|
|
||||||
try:
|
|
||||||
encoded_args = repr(module_args.encode('utf-8'))
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
encoded_args = repr(module_args)
|
|
||||||
try:
|
|
||||||
encoded_complex = repr(complex_args_json.encode('utf-8'))
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
encoded_complex = repr(complex_args_json.encode('utf-8'))
|
|
||||||
|
|
||||||
# these strings should be part of the 'basic' snippet which is required to be included
|
|
||||||
module_data = module_data.replace(REPLACER_VERSION, repr(__version__))
|
|
||||||
module_data = module_data.replace(REPLACER_SELINUX, ','.join(C.DEFAULT_SELINUX_SPECIAL_FS))
|
|
||||||
module_data = module_data.replace(REPLACER_ARGS, encoded_args)
|
|
||||||
module_data = module_data.replace(REPLACER_COMPLEX, encoded_complex)
|
|
||||||
|
|
||||||
if module_style == 'new':
|
|
||||||
facility = C.DEFAULT_SYSLOG_FACILITY
|
|
||||||
if 'ansible_syslog_facility' in inject:
|
|
||||||
facility = inject['ansible_syslog_facility']
|
|
||||||
module_data = module_data.replace('syslog.LOG_USER', "syslog.%s" % facility)
|
|
||||||
|
|
||||||
lines = module_data.split("\n")
|
|
||||||
shebang = None
|
|
||||||
if lines[0].startswith("#!"):
|
|
||||||
shebang = lines[0].strip()
|
|
||||||
args = shlex.split(str(shebang[2:]))
|
|
||||||
interpreter = args[0]
|
|
||||||
interpreter_config = 'ansible_%s_interpreter' % os.path.basename(interpreter)
|
|
||||||
|
|
||||||
if interpreter_config in inject:
|
|
||||||
interpreter = to_bytes(inject[interpreter_config], errors='strict')
|
|
||||||
lines[0] = shebang = "#!%s %s" % (interpreter, " ".join(args[1:]))
|
|
||||||
module_data = "\n".join(lines)
|
|
||||||
|
|
||||||
return (module_data, module_style, shebang)
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
# 2013, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
AXAPI_PORT_PROTOCOLS = {
|
|
||||||
'tcp': 2,
|
|
||||||
'udp': 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
AXAPI_VPORT_PROTOCOLS = {
|
|
||||||
'tcp': 2,
|
|
||||||
'udp': 3,
|
|
||||||
'fast-http': 9,
|
|
||||||
'http': 11,
|
|
||||||
'https': 12,
|
|
||||||
}
|
|
||||||
|
|
||||||
def a10_argument_spec():
|
|
||||||
return dict(
|
|
||||||
host=dict(type='str', required=True),
|
|
||||||
username=dict(type='str', aliases=['user', 'admin'], required=True),
|
|
||||||
password=dict(type='str', aliases=['pass', 'pwd'], required=True, no_log=True),
|
|
||||||
write_config=dict(type='bool', default=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
def axapi_failure(result):
|
|
||||||
if 'response' in result and result['response'].get('status') == 'fail':
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def axapi_call(module, url, post=None):
|
|
||||||
'''
|
|
||||||
Returns a datastructure based on the result of the API call
|
|
||||||
'''
|
|
||||||
rsp, info = fetch_url(module, url, data=post)
|
|
||||||
if not rsp or info['status'] >= 400:
|
|
||||||
module.fail_json(msg="failed to connect (status code %s), error was %s" % (info['status'], info.get('msg', 'no error given')))
|
|
||||||
try:
|
|
||||||
raw_data = rsp.read()
|
|
||||||
data = json.loads(raw_data)
|
|
||||||
except ValueError:
|
|
||||||
# at least one API call (system.action.write_config) returns
|
|
||||||
# XML even when JSON is requested, so do some minimal handling
|
|
||||||
# here to prevent failing even when the call succeeded
|
|
||||||
if 'status="ok"' in raw_data.lower():
|
|
||||||
data = {"response": {"status": "OK"}}
|
|
||||||
else:
|
|
||||||
data = {"response": {"status": "fail", "err": {"msg": raw_data}}}
|
|
||||||
except:
|
|
||||||
module.fail_json(msg="could not read the result from the host")
|
|
||||||
finally:
|
|
||||||
rsp.close()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def axapi_authenticate(module, base_url, username, password):
|
|
||||||
url = '%s&method=authenticate&username=%s&password=%s' % (base_url, username, password)
|
|
||||||
result = axapi_call(module, url)
|
|
||||||
if axapi_failure(result):
|
|
||||||
return module.fail_json(msg=result['response']['err']['msg'])
|
|
||||||
sessid = result['session_id']
|
|
||||||
return base_url + '&session_id=' + sessid
|
|
||||||
|
|
||||||
def axapi_enabled_disabled(flag):
|
|
||||||
'''
|
|
||||||
The axapi uses 0/1 integer values for flags, rather than strings
|
|
||||||
or booleans, so convert the given flag to a 0 or 1. For now, params
|
|
||||||
are specified as strings only so thats what we check.
|
|
||||||
'''
|
|
||||||
if flag == 'enabled':
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def axapi_get_port_protocol(protocol):
|
|
||||||
return AXAPI_PORT_PROTOCOLS.get(protocol.lower(), None)
|
|
||||||
|
|
||||||
def axapi_get_vport_protocol(protocol):
|
|
||||||
return AXAPI_VPORT_PROTOCOLS.get(protocol.lower(), None)
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -1,368 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# (c) 2015, René Moser <mail@renemoser.net>
|
|
||||||
#
|
|
||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from cs import CloudStack, CloudStackException, read_config
|
|
||||||
has_lib_cs = True
|
|
||||||
except ImportError:
|
|
||||||
has_lib_cs = False
|
|
||||||
|
|
||||||
|
|
||||||
class AnsibleCloudStack:
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
if not has_lib_cs:
|
|
||||||
module.fail_json(msg="python library cs required: pip install cs")
|
|
||||||
|
|
||||||
self.result = {
|
|
||||||
'changed': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.module = module
|
|
||||||
self._connect()
|
|
||||||
|
|
||||||
self.domain = None
|
|
||||||
self.account = None
|
|
||||||
self.project = None
|
|
||||||
self.ip_address = None
|
|
||||||
self.zone = None
|
|
||||||
self.vm = None
|
|
||||||
self.os_type = None
|
|
||||||
self.hypervisor = None
|
|
||||||
self.capabilities = None
|
|
||||||
|
|
||||||
|
|
||||||
def _connect(self):
|
|
||||||
api_key = self.module.params.get('api_key')
|
|
||||||
api_secret = self.module.params.get('secret_key')
|
|
||||||
api_url = self.module.params.get('api_url')
|
|
||||||
api_http_method = self.module.params.get('api_http_method')
|
|
||||||
api_timeout = self.module.params.get('api_timeout')
|
|
||||||
|
|
||||||
if api_key and api_secret and api_url:
|
|
||||||
self.cs = CloudStack(
|
|
||||||
endpoint=api_url,
|
|
||||||
key=api_key,
|
|
||||||
secret=api_secret,
|
|
||||||
timeout=api_timeout,
|
|
||||||
method=api_http_method
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.cs = CloudStack(**read_config())
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_fallback(self, key=None, fallback_key=None):
|
|
||||||
value = self.module.params.get(key)
|
|
||||||
if not value:
|
|
||||||
value = self.module.params.get(fallback_key)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: for backward compatibility only, remove if not used anymore
|
|
||||||
def _has_changed(self, want_dict, current_dict, only_keys=None):
|
|
||||||
return self.has_changed(want_dict=want_dict, current_dict=current_dict, only_keys=only_keys)
|
|
||||||
|
|
||||||
|
|
||||||
def has_changed(self, want_dict, current_dict, only_keys=None):
|
|
||||||
for key, value in want_dict.iteritems():
|
|
||||||
|
|
||||||
# Optionally limit by a list of keys
|
|
||||||
if only_keys and key not in only_keys:
|
|
||||||
continue;
|
|
||||||
|
|
||||||
# Skip None values
|
|
||||||
if value is None:
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if key in current_dict:
|
|
||||||
|
|
||||||
# API returns string for int in some cases, just to make sure
|
|
||||||
if isinstance(value, int):
|
|
||||||
current_dict[key] = int(current_dict[key])
|
|
||||||
elif isinstance(value, str):
|
|
||||||
current_dict[key] = str(current_dict[key])
|
|
||||||
|
|
||||||
# Only need to detect a singe change, not every item
|
|
||||||
if value != current_dict[key]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _get_by_key(self, key=None, my_dict={}):
|
|
||||||
if key:
|
|
||||||
if key in my_dict:
|
|
||||||
return my_dict[key]
|
|
||||||
self.module.fail_json(msg="Something went wrong: %s not found" % key)
|
|
||||||
return my_dict
|
|
||||||
|
|
||||||
|
|
||||||
def get_project(self, key=None):
|
|
||||||
if self.project:
|
|
||||||
return self._get_by_key(key, self.project)
|
|
||||||
|
|
||||||
project = self.module.params.get('project')
|
|
||||||
if not project:
|
|
||||||
return None
|
|
||||||
args = {}
|
|
||||||
args['account'] = self.get_account(key='name')
|
|
||||||
args['domainid'] = self.get_domain(key='id')
|
|
||||||
projects = self.cs.listProjects(**args)
|
|
||||||
if projects:
|
|
||||||
for p in projects['project']:
|
|
||||||
if project.lower() in [ p['name'].lower(), p['id'] ]:
|
|
||||||
self.project = p
|
|
||||||
return self._get_by_key(key, self.project)
|
|
||||||
self.module.fail_json(msg="project '%s' not found" % project)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ip_address(self, key=None):
|
|
||||||
if self.ip_address:
|
|
||||||
return self._get_by_key(key, self.ip_address)
|
|
||||||
|
|
||||||
ip_address = self.module.params.get('ip_address')
|
|
||||||
if not ip_address:
|
|
||||||
self.module.fail_json(msg="IP address param 'ip_address' is required")
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
args['ipaddress'] = ip_address
|
|
||||||
args['account'] = self.get_account(key='name')
|
|
||||||
args['domainid'] = self.get_domain(key='id')
|
|
||||||
args['projectid'] = self.get_project(key='id')
|
|
||||||
ip_addresses = self.cs.listPublicIpAddresses(**args)
|
|
||||||
|
|
||||||
if not ip_addresses:
|
|
||||||
self.module.fail_json(msg="IP address '%s' not found" % args['ipaddress'])
|
|
||||||
|
|
||||||
self.ip_address = ip_addresses['publicipaddress'][0]
|
|
||||||
return self._get_by_key(key, self.ip_address)
|
|
||||||
|
|
||||||
|
|
||||||
def get_vm(self, key=None):
|
|
||||||
if self.vm:
|
|
||||||
return self._get_by_key(key, self.vm)
|
|
||||||
|
|
||||||
vm = self.module.params.get('vm')
|
|
||||||
if not vm:
|
|
||||||
self.module.fail_json(msg="Virtual machine param 'vm' is required")
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
args['account'] = self.get_account(key='name')
|
|
||||||
args['domainid'] = self.get_domain(key='id')
|
|
||||||
args['projectid'] = self.get_project(key='id')
|
|
||||||
args['zoneid'] = self.get_zone(key='id')
|
|
||||||
vms = self.cs.listVirtualMachines(**args)
|
|
||||||
if vms:
|
|
||||||
for v in vms['virtualmachine']:
|
|
||||||
if vm in [ v['name'], v['displayname'], v['id'] ]:
|
|
||||||
self.vm = v
|
|
||||||
return self._get_by_key(key, self.vm)
|
|
||||||
self.module.fail_json(msg="Virtual machine '%s' not found" % vm)
|
|
||||||
|
|
||||||
|
|
||||||
def get_zone(self, key=None):
|
|
||||||
if self.zone:
|
|
||||||
return self._get_by_key(key, self.zone)
|
|
||||||
|
|
||||||
zone = self.module.params.get('zone')
|
|
||||||
zones = self.cs.listZones()
|
|
||||||
|
|
||||||
# use the first zone if no zone param given
|
|
||||||
if not zone:
|
|
||||||
self.zone = zones['zone'][0]
|
|
||||||
return self._get_by_key(key, self.zone)
|
|
||||||
|
|
||||||
if zones:
|
|
||||||
for z in zones['zone']:
|
|
||||||
if zone in [ z['name'], z['id'] ]:
|
|
||||||
self.zone = z
|
|
||||||
return self._get_by_key(key, self.zone)
|
|
||||||
self.module.fail_json(msg="zone '%s' not found" % zone)
|
|
||||||
|
|
||||||
|
|
||||||
def get_os_type(self, key=None):
|
|
||||||
if self.os_type:
|
|
||||||
return self._get_by_key(key, self.zone)
|
|
||||||
|
|
||||||
os_type = self.module.params.get('os_type')
|
|
||||||
if not os_type:
|
|
||||||
return None
|
|
||||||
|
|
||||||
os_types = self.cs.listOsTypes()
|
|
||||||
if os_types:
|
|
||||||
for o in os_types['ostype']:
|
|
||||||
if os_type in [ o['description'], o['id'] ]:
|
|
||||||
self.os_type = o
|
|
||||||
return self._get_by_key(key, self.os_type)
|
|
||||||
self.module.fail_json(msg="OS type '%s' not found" % os_type)
|
|
||||||
|
|
||||||
|
|
||||||
def get_hypervisor(self):
|
|
||||||
if self.hypervisor:
|
|
||||||
return self.hypervisor
|
|
||||||
|
|
||||||
hypervisor = self.module.params.get('hypervisor')
|
|
||||||
hypervisors = self.cs.listHypervisors()
|
|
||||||
|
|
||||||
# use the first hypervisor if no hypervisor param given
|
|
||||||
if not hypervisor:
|
|
||||||
self.hypervisor = hypervisors['hypervisor'][0]['name']
|
|
||||||
return self.hypervisor
|
|
||||||
|
|
||||||
for h in hypervisors['hypervisor']:
|
|
||||||
if hypervisor.lower() == h['name'].lower():
|
|
||||||
self.hypervisor = h['name']
|
|
||||||
return self.hypervisor
|
|
||||||
self.module.fail_json(msg="Hypervisor '%s' not found" % hypervisor)
|
|
||||||
|
|
||||||
|
|
||||||
def get_account(self, key=None):
|
|
||||||
if self.account:
|
|
||||||
return self._get_by_key(key, self.account)
|
|
||||||
|
|
||||||
account = self.module.params.get('account')
|
|
||||||
if not account:
|
|
||||||
return None
|
|
||||||
|
|
||||||
domain = self.module.params.get('domain')
|
|
||||||
if not domain:
|
|
||||||
self.module.fail_json(msg="Account must be specified with Domain")
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
args['name'] = account
|
|
||||||
args['domainid'] = self.get_domain(key='id')
|
|
||||||
args['listall'] = True
|
|
||||||
accounts = self.cs.listAccounts(**args)
|
|
||||||
if accounts:
|
|
||||||
self.account = accounts['account'][0]
|
|
||||||
return self._get_by_key(key, self.account)
|
|
||||||
self.module.fail_json(msg="Account '%s' not found" % account)
|
|
||||||
|
|
||||||
|
|
||||||
def get_domain(self, key=None):
|
|
||||||
if self.domain:
|
|
||||||
return self._get_by_key(key, self.domain)
|
|
||||||
|
|
||||||
domain = self.module.params.get('domain')
|
|
||||||
if not domain:
|
|
||||||
return None
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
args['listall'] = True
|
|
||||||
domains = self.cs.listDomains(**args)
|
|
||||||
if domains:
|
|
||||||
for d in domains['domain']:
|
|
||||||
if d['path'].lower() in [ domain.lower(), "root/" + domain.lower(), "root" + domain.lower() ]:
|
|
||||||
self.domain = d
|
|
||||||
return self._get_by_key(key, self.domain)
|
|
||||||
self.module.fail_json(msg="Domain '%s' not found" % domain)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tags(self, resource=None):
|
|
||||||
existing_tags = self.cs.listTags(resourceid=resource['id'])
|
|
||||||
if existing_tags:
|
|
||||||
return existing_tags['tag']
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_tags(self, resource, resource_type, tags):
|
|
||||||
existing_tags = resource['tags']
|
|
||||||
tags_to_delete = []
|
|
||||||
for existing_tag in existing_tags:
|
|
||||||
if existing_tag['key'] in tags:
|
|
||||||
if existing_tag['value'] != tags[key]:
|
|
||||||
tags_to_delete.append(existing_tag)
|
|
||||||
else:
|
|
||||||
tags_to_delete.append(existing_tag)
|
|
||||||
if tags_to_delete:
|
|
||||||
self.result['changed'] = True
|
|
||||||
if not self.module.check_mode:
|
|
||||||
args = {}
|
|
||||||
args['resourceids'] = resource['id']
|
|
||||||
args['resourcetype'] = resource_type
|
|
||||||
args['tags'] = tags_to_delete
|
|
||||||
self.cs.deleteTags(**args)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_tags(self, resource, resource_type, tags):
|
|
||||||
tags_to_create = []
|
|
||||||
for i, tag_entry in enumerate(tags):
|
|
||||||
tag = {
|
|
||||||
'key': tag_entry['key'],
|
|
||||||
'value': tag_entry['value'],
|
|
||||||
}
|
|
||||||
tags_to_create.append(tag)
|
|
||||||
if tags_to_create:
|
|
||||||
self.result['changed'] = True
|
|
||||||
if not self.module.check_mode:
|
|
||||||
args = {}
|
|
||||||
args['resourceids'] = resource['id']
|
|
||||||
args['resourcetype'] = resource_type
|
|
||||||
args['tags'] = tags_to_create
|
|
||||||
self.cs.createTags(**args)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_tags(self, resource, resource_type=None):
|
|
||||||
if not resource_type or not resource:
|
|
||||||
self.module.fail_json(msg="Error: Missing resource or resource_type for tags.")
|
|
||||||
|
|
||||||
if 'tags' in resource:
|
|
||||||
tags = self.module.params.get('tags')
|
|
||||||
if tags is not None:
|
|
||||||
self._delete_tags(resource, resource_type, tags)
|
|
||||||
self._create_tags(resource, resource_type, tags)
|
|
||||||
resource['tags'] = self.get_tags(resource)
|
|
||||||
return resource
|
|
||||||
|
|
||||||
|
|
||||||
def get_capabilities(self, key=None):
|
|
||||||
if self.capabilities:
|
|
||||||
return self._get_by_key(key, self.capabilities)
|
|
||||||
capabilities = self.cs.listCapabilities()
|
|
||||||
self.capabilities = capabilities['capability']
|
|
||||||
return self._get_by_key(key, self.capabilities)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: for backward compatibility only, remove if not used anymore
|
|
||||||
def _poll_job(self, job=None, key=None):
|
|
||||||
return self.poll_job(job=job, key=key)
|
|
||||||
|
|
||||||
|
|
||||||
def poll_job(self, job=None, key=None):
|
|
||||||
if 'jobid' in job:
|
|
||||||
while True:
|
|
||||||
res = self.cs.queryAsyncJobResult(jobid=job['jobid'])
|
|
||||||
if res['jobstatus'] != 0 and 'jobresult' in res:
|
|
||||||
if 'errortext' in res['jobresult']:
|
|
||||||
self.module.fail_json(msg="Failed: '%s'" % res['jobresult']['errortext'])
|
|
||||||
if key and key in res['jobresult']:
|
|
||||||
job = res['jobresult'][key]
|
|
||||||
break
|
|
||||||
time.sleep(2)
|
|
||||||
return job
|
|
@ -1,128 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
class SQLParseError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class UnclosedQuoteError(SQLParseError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# maps a type of identifier to the maximum number of dot levels that are
|
|
||||||
# allowed to specify that identifier. For example, a database column can be
|
|
||||||
# specified by up to 4 levels: database.schema.table.column
|
|
||||||
_PG_IDENTIFIER_TO_DOT_LEVEL = dict(database=1, schema=2, table=3, column=4, role=1)
|
|
||||||
_MYSQL_IDENTIFIER_TO_DOT_LEVEL = dict(database=1, table=2, column=3, role=1, vars=1)
|
|
||||||
|
|
||||||
def _find_end_quote(identifier, quote_char):
|
|
||||||
accumulate = 0
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
quote = identifier.index(quote_char)
|
|
||||||
except ValueError:
|
|
||||||
raise UnclosedQuoteError
|
|
||||||
accumulate = accumulate + quote
|
|
||||||
try:
|
|
||||||
next_char = identifier[quote+1]
|
|
||||||
except IndexError:
|
|
||||||
return accumulate
|
|
||||||
if next_char == quote_char:
|
|
||||||
try:
|
|
||||||
identifier = identifier[quote+2:]
|
|
||||||
accumulate = accumulate + 2
|
|
||||||
except IndexError:
|
|
||||||
raise UnclosedQuoteError
|
|
||||||
else:
|
|
||||||
return accumulate
|
|
||||||
|
|
||||||
|
|
||||||
def _identifier_parse(identifier, quote_char):
|
|
||||||
if not identifier:
|
|
||||||
raise SQLParseError('Identifier name unspecified or unquoted trailing dot')
|
|
||||||
|
|
||||||
already_quoted = False
|
|
||||||
if identifier.startswith(quote_char):
|
|
||||||
already_quoted = True
|
|
||||||
try:
|
|
||||||
end_quote = _find_end_quote(identifier[1:], quote_char=quote_char) + 1
|
|
||||||
except UnclosedQuoteError:
|
|
||||||
already_quoted = False
|
|
||||||
else:
|
|
||||||
if end_quote < len(identifier) - 1:
|
|
||||||
if identifier[end_quote+1] == '.':
|
|
||||||
dot = end_quote + 1
|
|
||||||
first_identifier = identifier[:dot]
|
|
||||||
next_identifier = identifier[dot+1:]
|
|
||||||
further_identifiers = _identifier_parse(next_identifier, quote_char)
|
|
||||||
further_identifiers.insert(0, first_identifier)
|
|
||||||
else:
|
|
||||||
raise SQLParseError('User escaped identifiers must escape extra quotes')
|
|
||||||
else:
|
|
||||||
further_identifiers = [identifier]
|
|
||||||
|
|
||||||
if not already_quoted:
|
|
||||||
try:
|
|
||||||
dot = identifier.index('.')
|
|
||||||
except ValueError:
|
|
||||||
identifier = identifier.replace(quote_char, quote_char*2)
|
|
||||||
identifier = ''.join((quote_char, identifier, quote_char))
|
|
||||||
further_identifiers = [identifier]
|
|
||||||
else:
|
|
||||||
if dot == 0 or dot >= len(identifier) - 1:
|
|
||||||
identifier = identifier.replace(quote_char, quote_char*2)
|
|
||||||
identifier = ''.join((quote_char, identifier, quote_char))
|
|
||||||
further_identifiers = [identifier]
|
|
||||||
else:
|
|
||||||
first_identifier = identifier[:dot]
|
|
||||||
next_identifier = identifier[dot+1:]
|
|
||||||
further_identifiers = _identifier_parse(next_identifier, quote_char)
|
|
||||||
first_identifier = first_identifier.replace(quote_char, quote_char*2)
|
|
||||||
first_identifier = ''.join((quote_char, first_identifier, quote_char))
|
|
||||||
further_identifiers.insert(0, first_identifier)
|
|
||||||
|
|
||||||
return further_identifiers
|
|
||||||
|
|
||||||
|
|
||||||
def pg_quote_identifier(identifier, id_type):
|
|
||||||
identifier_fragments = _identifier_parse(identifier, quote_char='"')
|
|
||||||
if len(identifier_fragments) > _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]:
|
|
||||||
raise SQLParseError('PostgreSQL does not support %s with more than %i dots' % (id_type, _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]))
|
|
||||||
return '.'.join(identifier_fragments)
|
|
||||||
|
|
||||||
def mysql_quote_identifier(identifier, id_type):
|
|
||||||
identifier_fragments = _identifier_parse(identifier, quote_char='`')
|
|
||||||
if len(identifier_fragments) > _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]:
|
|
||||||
raise SQLParseError('MySQL does not support %s with more than %i dots' % (id_type, _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]))
|
|
||||||
|
|
||||||
special_cased_fragments = []
|
|
||||||
for fragment in identifier_fragments:
|
|
||||||
if fragment == '`*`':
|
|
||||||
special_cased_fragments.append('*')
|
|
||||||
else:
|
|
||||||
special_cased_fragments.append(fragment)
|
|
||||||
|
|
||||||
return '.'.join(special_cased_fragments)
|
|
@ -1,188 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
try:
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
HAS_LOOSE_VERSION = True
|
|
||||||
except:
|
|
||||||
HAS_LOOSE_VERSION = False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def aws_common_argument_spec():
|
|
||||||
return dict(
|
|
||||||
ec2_url=dict(),
|
|
||||||
aws_secret_key=dict(aliases=['ec2_secret_key', 'secret_key'], no_log=True),
|
|
||||||
aws_access_key=dict(aliases=['ec2_access_key', 'access_key']),
|
|
||||||
validate_certs=dict(default=True, type='bool'),
|
|
||||||
security_token=dict(aliases=['access_token'], no_log=True),
|
|
||||||
profile=dict(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ec2_argument_spec():
|
|
||||||
spec = aws_common_argument_spec()
|
|
||||||
spec.update(
|
|
||||||
dict(
|
|
||||||
region=dict(aliases=['aws_region', 'ec2_region']),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return spec
|
|
||||||
|
|
||||||
|
|
||||||
def boto_supports_profile_name():
|
|
||||||
return hasattr(boto.ec2.EC2Connection, 'profile_name')
|
|
||||||
|
|
||||||
|
|
||||||
def get_aws_connection_info(module):
|
|
||||||
|
|
||||||
# Check module args for credentials, then check environment vars
|
|
||||||
# access_key
|
|
||||||
|
|
||||||
ec2_url = module.params.get('ec2_url')
|
|
||||||
access_key = module.params.get('aws_access_key')
|
|
||||||
secret_key = module.params.get('aws_secret_key')
|
|
||||||
security_token = module.params.get('security_token')
|
|
||||||
region = module.params.get('region')
|
|
||||||
profile_name = module.params.get('profile')
|
|
||||||
validate_certs = module.params.get('validate_certs')
|
|
||||||
|
|
||||||
if not ec2_url:
|
|
||||||
if 'AWS_URL' in os.environ:
|
|
||||||
ec2_url = os.environ['AWS_URL']
|
|
||||||
elif 'EC2_URL' in os.environ:
|
|
||||||
ec2_url = os.environ['EC2_URL']
|
|
||||||
|
|
||||||
if not access_key:
|
|
||||||
if 'AWS_ACCESS_KEY_ID' in os.environ:
|
|
||||||
access_key = os.environ['AWS_ACCESS_KEY_ID']
|
|
||||||
elif 'AWS_ACCESS_KEY' in os.environ:
|
|
||||||
access_key = os.environ['AWS_ACCESS_KEY']
|
|
||||||
elif 'EC2_ACCESS_KEY' in os.environ:
|
|
||||||
access_key = os.environ['EC2_ACCESS_KEY']
|
|
||||||
else:
|
|
||||||
# in case access_key came in as empty string
|
|
||||||
access_key = None
|
|
||||||
|
|
||||||
if not secret_key:
|
|
||||||
if 'AWS_SECRET_ACCESS_KEY' in os.environ:
|
|
||||||
secret_key = os.environ['AWS_SECRET_ACCESS_KEY']
|
|
||||||
elif 'AWS_SECRET_KEY' in os.environ:
|
|
||||||
secret_key = os.environ['AWS_SECRET_KEY']
|
|
||||||
elif 'EC2_SECRET_KEY' in os.environ:
|
|
||||||
secret_key = os.environ['EC2_SECRET_KEY']
|
|
||||||
else:
|
|
||||||
# in case secret_key came in as empty string
|
|
||||||
secret_key = None
|
|
||||||
|
|
||||||
if not region:
|
|
||||||
if 'AWS_REGION' in os.environ:
|
|
||||||
region = os.environ['AWS_REGION']
|
|
||||||
elif 'EC2_REGION' in os.environ:
|
|
||||||
region = os.environ['EC2_REGION']
|
|
||||||
else:
|
|
||||||
# boto.config.get returns None if config not found
|
|
||||||
region = boto.config.get('Boto', 'aws_region')
|
|
||||||
if not region:
|
|
||||||
region = boto.config.get('Boto', 'ec2_region')
|
|
||||||
|
|
||||||
if not security_token:
|
|
||||||
if 'AWS_SECURITY_TOKEN' in os.environ:
|
|
||||||
security_token = os.environ['AWS_SECURITY_TOKEN']
|
|
||||||
elif 'EC2_SECURITY_TOKEN' in os.environ:
|
|
||||||
security_token = os.environ['EC2_SECURITY_TOKEN']
|
|
||||||
else:
|
|
||||||
# in case security_token came in as empty string
|
|
||||||
security_token = None
|
|
||||||
|
|
||||||
boto_params = dict(aws_access_key_id=access_key,
|
|
||||||
aws_secret_access_key=secret_key,
|
|
||||||
security_token=security_token)
|
|
||||||
|
|
||||||
# profile_name only works as a key in boto >= 2.24
|
|
||||||
# so only set profile_name if passed as an argument
|
|
||||||
if profile_name:
|
|
||||||
if not boto_supports_profile_name():
|
|
||||||
module.fail_json("boto does not support profile_name before 2.24")
|
|
||||||
boto_params['profile_name'] = profile_name
|
|
||||||
|
|
||||||
if validate_certs and HAS_LOOSE_VERSION and LooseVersion(boto.Version) >= LooseVersion("2.6.0"):
|
|
||||||
boto_params['validate_certs'] = validate_certs
|
|
||||||
|
|
||||||
return region, ec2_url, boto_params
|
|
||||||
|
|
||||||
|
|
||||||
def get_ec2_creds(module):
|
|
||||||
''' for compatibility mode with old modules that don't/can't yet
|
|
||||||
use ec2_connect method '''
|
|
||||||
region, ec2_url, boto_params = get_aws_connection_info(module)
|
|
||||||
return ec2_url, boto_params['aws_access_key_id'], boto_params['aws_secret_access_key'], region
|
|
||||||
|
|
||||||
|
|
||||||
def boto_fix_security_token_in_profile(conn, profile_name):
|
|
||||||
''' monkey patch for boto issue boto/boto#2100 '''
|
|
||||||
profile = 'profile ' + profile_name
|
|
||||||
if boto.config.has_option(profile, 'aws_security_token'):
|
|
||||||
conn.provider.set_security_token(boto.config.get(profile, 'aws_security_token'))
|
|
||||||
return conn
|
|
||||||
|
|
||||||
|
|
||||||
def connect_to_aws(aws_module, region, **params):
|
|
||||||
conn = aws_module.connect_to_region(region, **params)
|
|
||||||
if not conn:
|
|
||||||
if region not in [aws_module_region.name for aws_module_region in aws_module.regions()]:
|
|
||||||
raise StandardError("Region %s does not seem to be available for aws module %s. If the region definitely exists, you may need to upgrade boto or extend with endpoints_path" % (region, aws_module.__name__))
|
|
||||||
else:
|
|
||||||
raise StandardError("Unknown problem connecting to region %s for aws module %s." % (region, aws_module.__name__))
|
|
||||||
if params.get('profile_name'):
|
|
||||||
conn = boto_fix_security_token_in_profile(conn, params['profile_name'])
|
|
||||||
return conn
|
|
||||||
|
|
||||||
|
|
||||||
def ec2_connect(module):
|
|
||||||
|
|
||||||
""" Return an ec2 connection"""
|
|
||||||
|
|
||||||
region, ec2_url, boto_params = get_aws_connection_info(module)
|
|
||||||
|
|
||||||
# If we have a region specified, connect to its endpoint.
|
|
||||||
if region:
|
|
||||||
try:
|
|
||||||
ec2 = connect_to_aws(boto.ec2, region, **boto_params)
|
|
||||||
except (boto.exception.NoAuthHandlerFound, StandardError), e:
|
|
||||||
module.fail_json(msg=str(e))
|
|
||||||
# Otherwise, no region so we fallback to the old connection method
|
|
||||||
elif ec2_url:
|
|
||||||
try:
|
|
||||||
ec2 = boto.connect_ec2_endpoint(ec2_url, **boto_params)
|
|
||||||
except (boto.exception.NoAuthHandlerFound, StandardError), e:
|
|
||||||
module.fail_json(msg=str(e))
|
|
||||||
else:
|
|
||||||
module.fail_json(msg="Either region or ec2_url must be specified")
|
|
||||||
|
|
||||||
return ec2
|
|
File diff suppressed because it is too large
Load Diff
@ -1,93 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Franck Cuny <franck.cuny@gmail.com>, 2014
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
|
|
||||||
import pprint
|
|
||||||
|
|
||||||
USER_AGENT_PRODUCT="Ansible-gce"
|
|
||||||
USER_AGENT_VERSION="v1"
|
|
||||||
|
|
||||||
def gce_connect(module, provider=None):
|
|
||||||
"""Return a Google Cloud Engine connection."""
|
|
||||||
service_account_email = module.params.get('service_account_email', None)
|
|
||||||
pem_file = module.params.get('pem_file', None)
|
|
||||||
project_id = module.params.get('project_id', None)
|
|
||||||
|
|
||||||
# If any of the values are not given as parameters, check the appropriate
|
|
||||||
# environment variables.
|
|
||||||
if not service_account_email:
|
|
||||||
service_account_email = os.environ.get('GCE_EMAIL', None)
|
|
||||||
if not project_id:
|
|
||||||
project_id = os.environ.get('GCE_PROJECT', None)
|
|
||||||
if not pem_file:
|
|
||||||
pem_file = os.environ.get('GCE_PEM_FILE_PATH', None)
|
|
||||||
|
|
||||||
# If we still don't have one or more of our credentials, attempt to
|
|
||||||
# get the remaining values from the libcloud secrets file.
|
|
||||||
if service_account_email is None or pem_file is None:
|
|
||||||
try:
|
|
||||||
import secrets
|
|
||||||
except ImportError:
|
|
||||||
secrets = None
|
|
||||||
|
|
||||||
if hasattr(secrets, 'GCE_PARAMS'):
|
|
||||||
if not service_account_email:
|
|
||||||
service_account_email = secrets.GCE_PARAMS[0]
|
|
||||||
if not pem_file:
|
|
||||||
pem_file = secrets.GCE_PARAMS[1]
|
|
||||||
keyword_params = getattr(secrets, 'GCE_KEYWORD_PARAMS', {})
|
|
||||||
if not project_id:
|
|
||||||
project_id = keyword_params.get('project', None)
|
|
||||||
|
|
||||||
# If we *still* don't have the credentials we need, then it's time to
|
|
||||||
# just fail out.
|
|
||||||
if service_account_email is None or pem_file is None or project_id is None:
|
|
||||||
module.fail_json(msg='Missing GCE connection parameters in libcloud '
|
|
||||||
'secrets file.')
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Allow for passing in libcloud Google DNS (e.g, Provider.GOOGLE)
|
|
||||||
if provider is None:
|
|
||||||
provider = Provider.GCE
|
|
||||||
|
|
||||||
try:
|
|
||||||
gce = get_driver(provider)(service_account_email, pem_file,
|
|
||||||
datacenter=module.params.get('zone', None),
|
|
||||||
project=project_id)
|
|
||||||
gce.connection.user_agent_append("%s/%s" % (
|
|
||||||
USER_AGENT_PRODUCT, USER_AGENT_VERSION))
|
|
||||||
except (RuntimeError, ValueError), e:
|
|
||||||
module.fail_json(msg=str(e), changed=False)
|
|
||||||
except Exception, e:
|
|
||||||
module.fail_json(msg=unexpected_error_msg(e), changed=False)
|
|
||||||
|
|
||||||
return gce
|
|
||||||
|
|
||||||
def unexpected_error_msg(error):
|
|
||||||
"""Create an error string based on passed in error."""
|
|
||||||
return 'Unexpected response: ' + pprint.pformat(vars(error))
|
|
@ -1,176 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
import hmac
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
try:
|
|
||||||
from hashlib import sha1
|
|
||||||
except ImportError:
|
|
||||||
import sha as sha1
|
|
||||||
|
|
||||||
HASHED_KEY_MAGIC = "|1|"
|
|
||||||
|
|
||||||
def add_git_host_key(module, url, accept_hostkey=True, create_dir=True):
|
|
||||||
|
|
||||||
""" idempotently add a git url hostkey """
|
|
||||||
|
|
||||||
fqdn = get_fqdn(url)
|
|
||||||
|
|
||||||
if fqdn:
|
|
||||||
known_host = check_hostkey(module, fqdn)
|
|
||||||
if not known_host:
|
|
||||||
if accept_hostkey:
|
|
||||||
rc, out, err = add_host_key(module, fqdn, create_dir=create_dir)
|
|
||||||
if rc != 0:
|
|
||||||
module.fail_json(msg="failed to add %s hostkey: %s" % (fqdn, out + err))
|
|
||||||
else:
|
|
||||||
module.fail_json(msg="%s has an unknown hostkey. Set accept_hostkey to True or manually add the hostkey prior to running the git module" % fqdn)
|
|
||||||
|
|
||||||
def get_fqdn(repo_url):
|
|
||||||
|
|
||||||
""" chop the hostname out of a giturl """
|
|
||||||
|
|
||||||
result = None
|
|
||||||
if "@" in repo_url and "://" not in repo_url:
|
|
||||||
# most likely a git@ or ssh+git@ type URL
|
|
||||||
repo_url = repo_url.split("@", 1)[1]
|
|
||||||
if ":" in repo_url:
|
|
||||||
repo_url = repo_url.split(":")[0]
|
|
||||||
result = repo_url
|
|
||||||
elif "/" in repo_url:
|
|
||||||
repo_url = repo_url.split("/")[0]
|
|
||||||
result = repo_url
|
|
||||||
elif "://" in repo_url:
|
|
||||||
# this should be something we can parse with urlparse
|
|
||||||
parts = urlparse.urlparse(repo_url)
|
|
||||||
if 'ssh' not in parts[0] and 'git' not in parts[0]:
|
|
||||||
# don't try and scan a hostname that's not ssh
|
|
||||||
return None
|
|
||||||
# parts[1] will be empty on python2.4 on ssh:// or git:// urls, so
|
|
||||||
# ensure we actually have a parts[1] before continuing.
|
|
||||||
if parts[1] != '':
|
|
||||||
result = parts[1]
|
|
||||||
if ":" in result:
|
|
||||||
result = result.split(":")[0]
|
|
||||||
if "@" in result:
|
|
||||||
result = result.split("@", 1)[1]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def check_hostkey(module, fqdn):
|
|
||||||
return not not_in_host_file(module, fqdn)
|
|
||||||
|
|
||||||
# this is a variant of code found in connection_plugins/paramiko.py and we should modify
|
|
||||||
# the paramiko code to import and use this.
|
|
||||||
|
|
||||||
def not_in_host_file(self, host):
|
|
||||||
|
|
||||||
|
|
||||||
if 'USER' in os.environ:
|
|
||||||
user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
|
|
||||||
else:
|
|
||||||
user_host_file = "~/.ssh/known_hosts"
|
|
||||||
user_host_file = os.path.expanduser(user_host_file)
|
|
||||||
|
|
||||||
host_file_list = []
|
|
||||||
host_file_list.append(user_host_file)
|
|
||||||
host_file_list.append("/etc/ssh/ssh_known_hosts")
|
|
||||||
host_file_list.append("/etc/ssh/ssh_known_hosts2")
|
|
||||||
|
|
||||||
hfiles_not_found = 0
|
|
||||||
for hf in host_file_list:
|
|
||||||
if not os.path.exists(hf):
|
|
||||||
hfiles_not_found += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
host_fh = open(hf)
|
|
||||||
except IOError, e:
|
|
||||||
hfiles_not_found += 1
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
data = host_fh.read()
|
|
||||||
host_fh.close()
|
|
||||||
|
|
||||||
for line in data.split("\n"):
|
|
||||||
if line is None or " " not in line:
|
|
||||||
continue
|
|
||||||
tokens = line.split()
|
|
||||||
if tokens[0].find(HASHED_KEY_MAGIC) == 0:
|
|
||||||
# this is a hashed known host entry
|
|
||||||
try:
|
|
||||||
(kn_salt,kn_host) = tokens[0][len(HASHED_KEY_MAGIC):].split("|",2)
|
|
||||||
hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
|
|
||||||
hash.update(host)
|
|
||||||
if hash.digest() == kn_host.decode('base64'):
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
# invalid hashed host key, skip it
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# standard host file entry
|
|
||||||
if host in tokens[0]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def add_host_key(module, fqdn, key_type="rsa", create_dir=False):
|
|
||||||
|
|
||||||
""" use ssh-keyscan to add the hostkey """
|
|
||||||
|
|
||||||
result = False
|
|
||||||
keyscan_cmd = module.get_bin_path('ssh-keyscan', True)
|
|
||||||
|
|
||||||
if 'USER' in os.environ:
|
|
||||||
user_ssh_dir = os.path.expandvars("~${USER}/.ssh/")
|
|
||||||
user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
|
|
||||||
else:
|
|
||||||
user_ssh_dir = "~/.ssh/"
|
|
||||||
user_host_file = "~/.ssh/known_hosts"
|
|
||||||
user_ssh_dir = os.path.expanduser(user_ssh_dir)
|
|
||||||
|
|
||||||
if not os.path.exists(user_ssh_dir):
|
|
||||||
if create_dir:
|
|
||||||
try:
|
|
||||||
os.makedirs(user_ssh_dir, 0700)
|
|
||||||
except:
|
|
||||||
module.fail_json(msg="failed to create host key directory: %s" % user_ssh_dir)
|
|
||||||
else:
|
|
||||||
module.fail_json(msg="%s does not exist" % user_ssh_dir)
|
|
||||||
elif not os.path.isdir(user_ssh_dir):
|
|
||||||
module.fail_json(msg="%s is not a directory" % user_ssh_dir)
|
|
||||||
|
|
||||||
this_cmd = "%s -t %s %s" % (keyscan_cmd, key_type, fqdn)
|
|
||||||
|
|
||||||
rc, out, err = module.run_command(this_cmd)
|
|
||||||
module.append_to_file(user_host_file, out)
|
|
||||||
|
|
||||||
return rc, out, err
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def openstack_argument_spec():
|
|
||||||
# DEPRECATED: This argument spec is only used for the deprecated old
|
|
||||||
# OpenStack modules. It turns out that modern OpenStack auth is WAY
|
|
||||||
# more complex than this.
|
|
||||||
# Consume standard OpenStack environment variables.
|
|
||||||
# This is mainly only useful for ad-hoc command line operation as
|
|
||||||
# in playbooks one would assume variables would be used appropriately
|
|
||||||
OS_AUTH_URL=os.environ.get('OS_AUTH_URL', 'http://127.0.0.1:35357/v2.0/')
|
|
||||||
OS_PASSWORD=os.environ.get('OS_PASSWORD', None)
|
|
||||||
OS_REGION_NAME=os.environ.get('OS_REGION_NAME', None)
|
|
||||||
OS_USERNAME=os.environ.get('OS_USERNAME', 'admin')
|
|
||||||
OS_TENANT_NAME=os.environ.get('OS_TENANT_NAME', OS_USERNAME)
|
|
||||||
|
|
||||||
spec = dict(
|
|
||||||
login_username = dict(default=OS_USERNAME),
|
|
||||||
auth_url = dict(default=OS_AUTH_URL),
|
|
||||||
region_name = dict(default=OS_REGION_NAME),
|
|
||||||
availability_zone = dict(default=None),
|
|
||||||
)
|
|
||||||
if OS_PASSWORD:
|
|
||||||
spec['login_password'] = dict(default=OS_PASSWORD)
|
|
||||||
else:
|
|
||||||
spec['login_password'] = dict(required=True)
|
|
||||||
if OS_TENANT_NAME:
|
|
||||||
spec['login_tenant_name'] = dict(default=OS_TENANT_NAME)
|
|
||||||
else:
|
|
||||||
spec['login_tenant_name'] = dict(required=True)
|
|
||||||
return spec
|
|
||||||
|
|
||||||
def openstack_find_nova_addresses(addresses, ext_tag, key_name=None):
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for (k, v) in addresses.iteritems():
|
|
||||||
if key_name and k == key_name:
|
|
||||||
ret.extend([addrs['addr'] for addrs in v])
|
|
||||||
else:
|
|
||||||
for interface_spec in v:
|
|
||||||
if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag:
|
|
||||||
ret.append(interface_spec['addr'])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def openstack_full_argument_spec(**kwargs):
|
|
||||||
spec = dict(
|
|
||||||
cloud=dict(default=None),
|
|
||||||
auth_type=dict(default=None),
|
|
||||||
auth=dict(default=None),
|
|
||||||
region_name=dict(default=None),
|
|
||||||
availability_zone=dict(default=None),
|
|
||||||
verify=dict(default=True, aliases=['validate_certs']),
|
|
||||||
cacert=dict(default=None),
|
|
||||||
cert=dict(default=None),
|
|
||||||
key=dict(default=None),
|
|
||||||
wait=dict(default=True, type='bool'),
|
|
||||||
timeout=dict(default=180, type='int'),
|
|
||||||
api_timeout=dict(default=None, type='int'),
|
|
||||||
endpoint_type=dict(
|
|
||||||
default='public', choices=['public', 'internal', 'admin']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
spec.update(kwargs)
|
|
||||||
return spec
|
|
||||||
|
|
||||||
|
|
||||||
def openstack_module_kwargs(**kwargs):
|
|
||||||
ret = {}
|
|
||||||
for key in ('mutually_exclusive', 'required_together', 'required_one_of'):
|
|
||||||
if key in kwargs:
|
|
||||||
if key in ret:
|
|
||||||
ret[key].extend(kwargs[key])
|
|
||||||
else:
|
|
||||||
ret[key] = kwargs[key]
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,166 +0,0 @@
|
|||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2014, and others
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
#
|
|
||||||
|
|
||||||
# Helper function to parse Ansible JSON arguments from a file passed as
|
|
||||||
# the single argument to the module
|
|
||||||
# Example: $params = Parse-Args $args
|
|
||||||
Function Parse-Args($arguments)
|
|
||||||
{
|
|
||||||
$parameters = New-Object psobject;
|
|
||||||
If ($arguments.Length -gt 0)
|
|
||||||
{
|
|
||||||
$parameters = Get-Content $arguments[0] | ConvertFrom-Json;
|
|
||||||
}
|
|
||||||
$parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to set an "attribute" on a psobject instance in powershell.
|
|
||||||
# This is a convenience to make adding Members to the object easier and
|
|
||||||
# slightly more pythonic
|
|
||||||
# Example: Set-Attr $result "changed" $true
|
|
||||||
Function Set-Attr($obj, $name, $value)
|
|
||||||
{
|
|
||||||
# If the provided $obj is undefined, define one to be nice
|
|
||||||
If (-not $obj.GetType)
|
|
||||||
{
|
|
||||||
$obj = New-Object psobject
|
|
||||||
}
|
|
||||||
|
|
||||||
$obj | Add-Member -Force -MemberType NoteProperty -Name $name -Value $value
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to convert a powershell object to JSON to echo it, exiting
|
|
||||||
# the script
|
|
||||||
# Example: Exit-Json $result
|
|
||||||
Function Exit-Json($obj)
|
|
||||||
{
|
|
||||||
# If the provided $obj is undefined, define one to be nice
|
|
||||||
If (-not $obj.GetType)
|
|
||||||
{
|
|
||||||
$obj = New-Object psobject
|
|
||||||
}
|
|
||||||
|
|
||||||
echo $obj | ConvertTo-Json -Compress -Depth 99
|
|
||||||
Exit
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to add the "msg" property and "failed" property, convert the
|
|
||||||
# powershell object to JSON and echo it, exiting the script
|
|
||||||
# Example: Fail-Json $result "This is the failure message"
|
|
||||||
Function Fail-Json($obj, $message = $null)
|
|
||||||
{
|
|
||||||
# If we weren't given 2 args, and the only arg was a string, create a new
|
|
||||||
# psobject and use the arg as the failure message
|
|
||||||
If ($message -eq $null -and $obj.GetType().Name -eq "String")
|
|
||||||
{
|
|
||||||
$message = $obj
|
|
||||||
$obj = New-Object psobject
|
|
||||||
}
|
|
||||||
# If the first args is undefined or not an object, make it an object
|
|
||||||
ElseIf (-not $obj.GetType -or $obj.GetType().Name -ne "PSCustomObject")
|
|
||||||
{
|
|
||||||
$obj = New-Object psobject
|
|
||||||
}
|
|
||||||
|
|
||||||
Set-Attr $obj "msg" $message
|
|
||||||
Set-Attr $obj "failed" $true
|
|
||||||
echo $obj | ConvertTo-Json -Compress -Depth 99
|
|
||||||
Exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to get an "attribute" from a psobject instance in powershell.
|
|
||||||
# This is a convenience to make getting Members from an object easier and
|
|
||||||
# slightly more pythonic
|
|
||||||
# Example: $attr = Get-Attr $response "code" -default "1"
|
|
||||||
#Note that if you use the failifempty option, you do need to specify resultobject as well.
|
|
||||||
Function Get-Attr($obj, $name, $default = $null,$resultobj, $failifempty=$false, $emptyattributefailmessage)
|
|
||||||
{
|
|
||||||
# Check if the provided Member $name exists in $obj and return it or the
|
|
||||||
# default
|
|
||||||
If ($obj.$name.GetType)
|
|
||||||
{
|
|
||||||
$obj.$name
|
|
||||||
}
|
|
||||||
Elseif($failifempty -eq $false)
|
|
||||||
{
|
|
||||||
$default
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!$emptyattributefailmessage) {$emptyattributefailmessage = "Missing required argument: $name"}
|
|
||||||
Fail-Json -obj $resultobj -message $emptyattributefailmessage
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper filter/pipeline function to convert a value to boolean following current
|
|
||||||
# Ansible practices
|
|
||||||
# Example: $is_true = "true" | ConvertTo-Bool
|
|
||||||
Function ConvertTo-Bool
|
|
||||||
{
|
|
||||||
param(
|
|
||||||
[parameter(valuefrompipeline=$true)]
|
|
||||||
$obj
|
|
||||||
)
|
|
||||||
|
|
||||||
$boolean_strings = "yes", "on", "1", "true", 1
|
|
||||||
$obj_string = [string]$obj
|
|
||||||
|
|
||||||
if (($obj.GetType().Name -eq "Boolean" -and $obj) -or $boolean_strings -contains $obj_string.ToLower())
|
|
||||||
{
|
|
||||||
$true
|
|
||||||
}
|
|
||||||
Else
|
|
||||||
{
|
|
||||||
$false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to calculate a hash of a file in a way which powershell 3
|
|
||||||
# and above can handle:
|
|
||||||
Function Get-FileChecksum($path)
|
|
||||||
{
|
|
||||||
$hash = ""
|
|
||||||
If (Test-Path -PathType Leaf $path)
|
|
||||||
{
|
|
||||||
$sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider;
|
|
||||||
$fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read);
|
|
||||||
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
|
|
||||||
$fp.Dispose();
|
|
||||||
}
|
|
||||||
ElseIf (Test-Path -PathType Container $path)
|
|
||||||
{
|
|
||||||
$hash= "3";
|
|
||||||
}
|
|
||||||
Else
|
|
||||||
{
|
|
||||||
$hash = "1";
|
|
||||||
}
|
|
||||||
return $hash
|
|
||||||
}
|
|
@ -1,328 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by
|
|
||||||
# Ansible still belong to the author of the module, and may assign their own
|
|
||||||
# license to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without
|
|
||||||
# modification, are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer in the
|
|
||||||
# documentation and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
||||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
|
|
||||||
FINAL_STATUSES = ('ACTIVE', 'ERROR')
|
|
||||||
VOLUME_STATUS = ('available', 'attaching', 'creating', 'deleting', 'in-use',
|
|
||||||
'error', 'error_deleting')
|
|
||||||
|
|
||||||
CLB_ALGORITHMS = ['RANDOM', 'LEAST_CONNECTIONS', 'ROUND_ROBIN',
|
|
||||||
'WEIGHTED_LEAST_CONNECTIONS', 'WEIGHTED_ROUND_ROBIN']
|
|
||||||
CLB_PROTOCOLS = ['DNS_TCP', 'DNS_UDP', 'FTP', 'HTTP', 'HTTPS', 'IMAPS',
|
|
||||||
'IMAPv4', 'LDAP', 'LDAPS', 'MYSQL', 'POP3', 'POP3S', 'SMTP',
|
|
||||||
'TCP', 'TCP_CLIENT_FIRST', 'UDP', 'UDP_STREAM', 'SFTP']
|
|
||||||
|
|
||||||
NON_CALLABLES = (basestring, bool, dict, int, list, type(None))
|
|
||||||
PUBLIC_NET_ID = "00000000-0000-0000-0000-000000000000"
|
|
||||||
SERVICE_NET_ID = "11111111-1111-1111-1111-111111111111"
|
|
||||||
|
|
||||||
|
|
||||||
def rax_slugify(value):
|
|
||||||
"""Prepend a key with rax_ and normalize the key name"""
|
|
||||||
return 'rax_%s' % (re.sub('[^\w-]', '_', value).lower().lstrip('_'))
|
|
||||||
|
|
||||||
|
|
||||||
def rax_clb_node_to_dict(obj):
|
|
||||||
"""Function to convert a CLB Node object to a dict"""
|
|
||||||
if not obj:
|
|
||||||
return {}
|
|
||||||
node = obj.to_dict()
|
|
||||||
node['id'] = obj.id
|
|
||||||
node['weight'] = obj.weight
|
|
||||||
return node
|
|
||||||
|
|
||||||
|
|
||||||
def rax_to_dict(obj, obj_type='standard'):
|
|
||||||
"""Generic function to convert a pyrax object to a dict
|
|
||||||
|
|
||||||
obj_type values:
|
|
||||||
standard
|
|
||||||
clb
|
|
||||||
server
|
|
||||||
|
|
||||||
"""
|
|
||||||
instance = {}
|
|
||||||
for key in dir(obj):
|
|
||||||
value = getattr(obj, key)
|
|
||||||
if obj_type == 'clb' and key == 'nodes':
|
|
||||||
instance[key] = []
|
|
||||||
for node in value:
|
|
||||||
instance[key].append(rax_clb_node_to_dict(node))
|
|
||||||
elif (isinstance(value, list) and len(value) > 0 and
|
|
||||||
not isinstance(value[0], NON_CALLABLES)):
|
|
||||||
instance[key] = []
|
|
||||||
for item in value:
|
|
||||||
instance[key].append(rax_to_dict(item))
|
|
||||||
elif (isinstance(value, NON_CALLABLES) and not key.startswith('_')):
|
|
||||||
if obj_type == 'server':
|
|
||||||
if key == 'image':
|
|
||||||
if not value:
|
|
||||||
instance['rax_boot_source'] = 'volume'
|
|
||||||
else:
|
|
||||||
instance['rax_boot_source'] = 'local'
|
|
||||||
key = rax_slugify(key)
|
|
||||||
instance[key] = value
|
|
||||||
|
|
||||||
if obj_type == 'server':
|
|
||||||
for attr in ['id', 'accessIPv4', 'name', 'status']:
|
|
||||||
instance[attr] = instance.get(rax_slugify(attr))
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
def rax_find_bootable_volume(module, rax_module, server, exit=True):
|
|
||||||
"""Find a servers bootable volume"""
|
|
||||||
cs = rax_module.cloudservers
|
|
||||||
cbs = rax_module.cloud_blockstorage
|
|
||||||
server_id = rax_module.utils.get_id(server)
|
|
||||||
volumes = cs.volumes.get_server_volumes(server_id)
|
|
||||||
bootable_volumes = []
|
|
||||||
for volume in volumes:
|
|
||||||
vol = cbs.get(volume)
|
|
||||||
if module.boolean(vol.bootable):
|
|
||||||
bootable_volumes.append(vol)
|
|
||||||
if not bootable_volumes:
|
|
||||||
if exit:
|
|
||||||
module.fail_json(msg='No bootable volumes could be found for '
|
|
||||||
'server %s' % server_id)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
elif len(bootable_volumes) > 1:
|
|
||||||
if exit:
|
|
||||||
module.fail_json(msg='Multiple bootable volumes found for server '
|
|
||||||
'%s' % server_id)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return bootable_volumes[0]
|
|
||||||
|
|
||||||
|
|
||||||
def rax_find_image(module, rax_module, image, exit=True):
|
|
||||||
"""Find a server image by ID or Name"""
|
|
||||||
cs = rax_module.cloudservers
|
|
||||||
try:
|
|
||||||
UUID(image)
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
image = cs.images.find(human_id=image)
|
|
||||||
except(cs.exceptions.NotFound,
|
|
||||||
cs.exceptions.NoUniqueMatch):
|
|
||||||
try:
|
|
||||||
image = cs.images.find(name=image)
|
|
||||||
except (cs.exceptions.NotFound,
|
|
||||||
cs.exceptions.NoUniqueMatch):
|
|
||||||
if exit:
|
|
||||||
module.fail_json(msg='No matching image found (%s)' %
|
|
||||||
image)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return rax_module.utils.get_id(image)
|
|
||||||
|
|
||||||
|
|
||||||
def rax_find_volume(module, rax_module, name):
|
|
||||||
"""Find a Block storage volume by ID or name"""
|
|
||||||
cbs = rax_module.cloud_blockstorage
|
|
||||||
try:
|
|
||||||
UUID(name)
|
|
||||||
volume = cbs.get(name)
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
volume = cbs.find(name=name)
|
|
||||||
except rax_module.exc.NotFound:
|
|
||||||
volume = None
|
|
||||||
except Exception, e:
|
|
||||||
module.fail_json(msg='%s' % e)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
|
|
||||||
def rax_find_network(module, rax_module, network):
|
|
||||||
"""Find a cloud network by ID or name"""
|
|
||||||
cnw = rax_module.cloud_networks
|
|
||||||
try:
|
|
||||||
UUID(network)
|
|
||||||
except ValueError:
|
|
||||||
if network.lower() == 'public':
|
|
||||||
return cnw.get_server_networks(PUBLIC_NET_ID)
|
|
||||||
elif network.lower() == 'private':
|
|
||||||
return cnw.get_server_networks(SERVICE_NET_ID)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
network_obj = cnw.find_network_by_label(network)
|
|
||||||
except (rax_module.exceptions.NetworkNotFound,
|
|
||||||
rax_module.exceptions.NetworkLabelNotUnique):
|
|
||||||
module.fail_json(msg='No matching network found (%s)' %
|
|
||||||
network)
|
|
||||||
else:
|
|
||||||
return cnw.get_server_networks(network_obj)
|
|
||||||
else:
|
|
||||||
return cnw.get_server_networks(network)
|
|
||||||
|
|
||||||
|
|
||||||
def rax_find_server(module, rax_module, server):
|
|
||||||
"""Find a Cloud Server by ID or name"""
|
|
||||||
cs = rax_module.cloudservers
|
|
||||||
try:
|
|
||||||
UUID(server)
|
|
||||||
server = cs.servers.get(server)
|
|
||||||
except ValueError:
|
|
||||||
servers = cs.servers.list(search_opts=dict(name='^%s$' % server))
|
|
||||||
if not servers:
|
|
||||||
module.fail_json(msg='No Server was matched by name, '
|
|
||||||
'try using the Server ID instead')
|
|
||||||
if len(servers) > 1:
|
|
||||||
module.fail_json(msg='Multiple servers matched by name, '
|
|
||||||
'try using the Server ID instead')
|
|
||||||
|
|
||||||
# We made it this far, grab the first and hopefully only server
|
|
||||||
# in the list
|
|
||||||
server = servers[0]
|
|
||||||
return server
|
|
||||||
|
|
||||||
|
|
||||||
def rax_find_loadbalancer(module, rax_module, loadbalancer):
|
|
||||||
"""Find a Cloud Load Balancer by ID or name"""
|
|
||||||
clb = rax_module.cloud_loadbalancers
|
|
||||||
try:
|
|
||||||
found = clb.get(loadbalancer)
|
|
||||||
except:
|
|
||||||
found = []
|
|
||||||
for lb in clb.list():
|
|
||||||
if loadbalancer == lb.name:
|
|
||||||
found.append(lb)
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
module.fail_json(msg='No loadbalancer was matched')
|
|
||||||
|
|
||||||
if len(found) > 1:
|
|
||||||
module.fail_json(msg='Multiple loadbalancers matched')
|
|
||||||
|
|
||||||
# We made it this far, grab the first and hopefully only item
|
|
||||||
# in the list
|
|
||||||
found = found[0]
|
|
||||||
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
def rax_argument_spec():
|
|
||||||
"""Return standard base dictionary used for the argument_spec
|
|
||||||
argument in AnsibleModule
|
|
||||||
|
|
||||||
"""
|
|
||||||
return dict(
|
|
||||||
api_key=dict(type='str', aliases=['password'], no_log=True),
|
|
||||||
auth_endpoint=dict(type='str'),
|
|
||||||
credentials=dict(type='str', aliases=['creds_file']),
|
|
||||||
env=dict(type='str'),
|
|
||||||
identity_type=dict(type='str', default='rackspace'),
|
|
||||||
region=dict(type='str'),
|
|
||||||
tenant_id=dict(type='str'),
|
|
||||||
tenant_name=dict(type='str'),
|
|
||||||
username=dict(type='str'),
|
|
||||||
verify_ssl=dict(choices=BOOLEANS, type='bool'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def rax_required_together():
|
|
||||||
"""Return the default list used for the required_together argument to
|
|
||||||
AnsibleModule"""
|
|
||||||
return [['api_key', 'username']]
|
|
||||||
|
|
||||||
|
|
||||||
def setup_rax_module(module, rax_module, region_required=True):
|
|
||||||
"""Set up pyrax in a standard way for all modules"""
|
|
||||||
rax_module.USER_AGENT = 'ansible/%s %s' % (ANSIBLE_VERSION,
|
|
||||||
rax_module.USER_AGENT)
|
|
||||||
|
|
||||||
api_key = module.params.get('api_key')
|
|
||||||
auth_endpoint = module.params.get('auth_endpoint')
|
|
||||||
credentials = module.params.get('credentials')
|
|
||||||
env = module.params.get('env')
|
|
||||||
identity_type = module.params.get('identity_type')
|
|
||||||
region = module.params.get('region')
|
|
||||||
tenant_id = module.params.get('tenant_id')
|
|
||||||
tenant_name = module.params.get('tenant_name')
|
|
||||||
username = module.params.get('username')
|
|
||||||
verify_ssl = module.params.get('verify_ssl')
|
|
||||||
|
|
||||||
if env is not None:
|
|
||||||
rax_module.set_environment(env)
|
|
||||||
|
|
||||||
rax_module.set_setting('identity_type', identity_type)
|
|
||||||
if verify_ssl is not None:
|
|
||||||
rax_module.set_setting('verify_ssl', verify_ssl)
|
|
||||||
if auth_endpoint is not None:
|
|
||||||
rax_module.set_setting('auth_endpoint', auth_endpoint)
|
|
||||||
if tenant_id is not None:
|
|
||||||
rax_module.set_setting('tenant_id', tenant_id)
|
|
||||||
if tenant_name is not None:
|
|
||||||
rax_module.set_setting('tenant_name', tenant_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
username = username or os.environ.get('RAX_USERNAME')
|
|
||||||
if not username:
|
|
||||||
username = rax_module.get_setting('keyring_username')
|
|
||||||
if username:
|
|
||||||
api_key = 'USE_KEYRING'
|
|
||||||
if not api_key:
|
|
||||||
api_key = os.environ.get('RAX_API_KEY')
|
|
||||||
credentials = (credentials or os.environ.get('RAX_CREDENTIALS') or
|
|
||||||
os.environ.get('RAX_CREDS_FILE'))
|
|
||||||
region = (region or os.environ.get('RAX_REGION') or
|
|
||||||
rax_module.get_setting('region'))
|
|
||||||
except KeyError, e:
|
|
||||||
module.fail_json(msg='Unable to load %s' % e.message)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if api_key and username:
|
|
||||||
if api_key == 'USE_KEYRING':
|
|
||||||
rax_module.keyring_auth(username, region=region)
|
|
||||||
else:
|
|
||||||
rax_module.set_credentials(username, api_key=api_key,
|
|
||||||
region=region)
|
|
||||||
elif credentials:
|
|
||||||
credentials = os.path.expanduser(credentials)
|
|
||||||
rax_module.set_credential_file(credentials, region=region)
|
|
||||||
else:
|
|
||||||
raise Exception('No credentials supplied!')
|
|
||||||
except Exception, e:
|
|
||||||
if e.message:
|
|
||||||
msg = str(e.message)
|
|
||||||
else:
|
|
||||||
msg = repr(e)
|
|
||||||
module.fail_json(msg=msg)
|
|
||||||
|
|
||||||
if region_required and region not in rax_module.regions:
|
|
||||||
module.fail_json(msg='%s is not a valid region, must be one of: %s' %
|
|
||||||
(region, ','.join(rax_module.regions)))
|
|
||||||
|
|
||||||
return rax_module
|
|
@ -1,280 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), James Laska
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import types
|
|
||||||
import ConfigParser
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationBase(object):
|
|
||||||
def __init__(self, module, username=None, password=None):
|
|
||||||
self.module = module
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
raise NotImplementedError("Must be implemented by a sub-class")
|
|
||||||
|
|
||||||
def enable(self):
|
|
||||||
# Remove any existing redhat.repo
|
|
||||||
redhat_repo = '/etc/yum.repos.d/redhat.repo'
|
|
||||||
if os.path.isfile(redhat_repo):
|
|
||||||
os.unlink(redhat_repo)
|
|
||||||
|
|
||||||
def register(self):
|
|
||||||
raise NotImplementedError("Must be implemented by a sub-class")
|
|
||||||
|
|
||||||
def unregister(self):
|
|
||||||
raise NotImplementedError("Must be implemented by a sub-class")
|
|
||||||
|
|
||||||
def unsubscribe(self):
|
|
||||||
raise NotImplementedError("Must be implemented by a sub-class")
|
|
||||||
|
|
||||||
def update_plugin_conf(self, plugin, enabled=True):
|
|
||||||
plugin_conf = '/etc/yum/pluginconf.d/%s.conf' % plugin
|
|
||||||
if os.path.isfile(plugin_conf):
|
|
||||||
cfg = ConfigParser.ConfigParser()
|
|
||||||
cfg.read([plugin_conf])
|
|
||||||
if enabled:
|
|
||||||
cfg.set('main', 'enabled', 1)
|
|
||||||
else:
|
|
||||||
cfg.set('main', 'enabled', 0)
|
|
||||||
fd = open(plugin_conf, 'rwa+')
|
|
||||||
cfg.write(fd)
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
def subscribe(self, **kwargs):
|
|
||||||
raise NotImplementedError("Must be implemented by a sub-class")
|
|
||||||
|
|
||||||
|
|
||||||
class Rhsm(RegistrationBase):
|
|
||||||
def __init__(self, module, username=None, password=None):
|
|
||||||
RegistrationBase.__init__(self, module, username, password)
|
|
||||||
self.config = self._read_config()
|
|
||||||
self.module = module
|
|
||||||
|
|
||||||
def _read_config(self, rhsm_conf='/etc/rhsm/rhsm.conf'):
|
|
||||||
'''
|
|
||||||
Load RHSM configuration from /etc/rhsm/rhsm.conf.
|
|
||||||
Returns:
|
|
||||||
* ConfigParser object
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Read RHSM defaults ...
|
|
||||||
cp = ConfigParser.ConfigParser()
|
|
||||||
cp.read(rhsm_conf)
|
|
||||||
|
|
||||||
# Add support for specifying a default value w/o having to standup some configuration
|
|
||||||
# Yeah, I know this should be subclassed ... but, oh well
|
|
||||||
def get_option_default(self, key, default=''):
|
|
||||||
sect, opt = key.split('.', 1)
|
|
||||||
if self.has_section(sect) and self.has_option(sect, opt):
|
|
||||||
return self.get(sect, opt)
|
|
||||||
else:
|
|
||||||
return default
|
|
||||||
|
|
||||||
cp.get_option = types.MethodType(get_option_default, cp, ConfigParser.ConfigParser)
|
|
||||||
|
|
||||||
return cp
|
|
||||||
|
|
||||||
def enable(self):
|
|
||||||
'''
|
|
||||||
Enable the system to receive updates from subscription-manager.
|
|
||||||
This involves updating affected yum plugins and removing any
|
|
||||||
conflicting yum repositories.
|
|
||||||
'''
|
|
||||||
RegistrationBase.enable(self)
|
|
||||||
self.update_plugin_conf('rhnplugin', False)
|
|
||||||
self.update_plugin_conf('subscription-manager', True)
|
|
||||||
|
|
||||||
def configure(self, **kwargs):
|
|
||||||
'''
|
|
||||||
Configure the system as directed for registration with RHN
|
|
||||||
Raises:
|
|
||||||
* Exception - if error occurs while running command
|
|
||||||
'''
|
|
||||||
args = ['subscription-manager', 'config']
|
|
||||||
|
|
||||||
# Pass supplied **kwargs as parameters to subscription-manager. Ignore
|
|
||||||
# non-configuration parameters and replace '_' with '.'. For example,
|
|
||||||
# 'server_hostname' becomes '--system.hostname'.
|
|
||||||
for k,v in kwargs.items():
|
|
||||||
if re.search(r'^(system|rhsm)_', k):
|
|
||||||
args.append('--%s=%s' % (k.replace('_','.'), v))
|
|
||||||
|
|
||||||
self.module.run_command(args, check_rc=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_registered(self):
|
|
||||||
'''
|
|
||||||
Determine whether the current system
|
|
||||||
Returns:
|
|
||||||
* Boolean - whether the current system is currently registered to
|
|
||||||
RHN.
|
|
||||||
'''
|
|
||||||
# Quick version...
|
|
||||||
if False:
|
|
||||||
return os.path.isfile('/etc/pki/consumer/cert.pem') and \
|
|
||||||
os.path.isfile('/etc/pki/consumer/key.pem')
|
|
||||||
|
|
||||||
args = ['subscription-manager', 'identity']
|
|
||||||
rc, stdout, stderr = self.module.run_command(args, check_rc=False)
|
|
||||||
if rc == 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def register(self, username, password, autosubscribe, activationkey):
|
|
||||||
'''
|
|
||||||
Register the current system to the provided RHN server
|
|
||||||
Raises:
|
|
||||||
* Exception - if error occurs while running command
|
|
||||||
'''
|
|
||||||
args = ['subscription-manager', 'register']
|
|
||||||
|
|
||||||
# Generate command arguments
|
|
||||||
if activationkey:
|
|
||||||
args.append('--activationkey "%s"' % activationkey)
|
|
||||||
else:
|
|
||||||
if autosubscribe:
|
|
||||||
args.append('--autosubscribe')
|
|
||||||
if username:
|
|
||||||
args.extend(['--username', username])
|
|
||||||
if password:
|
|
||||||
args.extend(['--password', password])
|
|
||||||
|
|
||||||
# Do the needful...
|
|
||||||
rc, stderr, stdout = self.module.run_command(args, check_rc=True)
|
|
||||||
|
|
||||||
def unsubscribe(self):
|
|
||||||
'''
|
|
||||||
Unsubscribe a system from all subscribed channels
|
|
||||||
Raises:
|
|
||||||
* Exception - if error occurs while running command
|
|
||||||
'''
|
|
||||||
args = ['subscription-manager', 'unsubscribe', '--all']
|
|
||||||
rc, stderr, stdout = self.module.run_command(args, check_rc=True)
|
|
||||||
|
|
||||||
def unregister(self):
|
|
||||||
'''
|
|
||||||
Unregister a currently registered system
|
|
||||||
Raises:
|
|
||||||
* Exception - if error occurs while running command
|
|
||||||
'''
|
|
||||||
args = ['subscription-manager', 'unregister']
|
|
||||||
rc, stderr, stdout = self.module.run_command(args, check_rc=True)
|
|
||||||
|
|
||||||
def subscribe(self, regexp):
|
|
||||||
'''
|
|
||||||
Subscribe current system to available pools matching the specified
|
|
||||||
regular expression
|
|
||||||
Raises:
|
|
||||||
* Exception - if error occurs while running command
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Available pools ready for subscription
|
|
||||||
available_pools = RhsmPools(self.module)
|
|
||||||
|
|
||||||
for pool in available_pools.filter(regexp):
|
|
||||||
pool.subscribe()
|
|
||||||
|
|
||||||
|
|
||||||
class RhsmPool(object):
|
|
||||||
'''
|
|
||||||
Convenience class for housing subscription information
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, module, **kwargs):
|
|
||||||
self.module = module
|
|
||||||
for k,v in kwargs.items():
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.__getattribute__('_name'))
|
|
||||||
|
|
||||||
def subscribe(self):
|
|
||||||
args = "subscription-manager subscribe --pool %s" % self.PoolId
|
|
||||||
rc, stdout, stderr = self.module.run_command(args, check_rc=True)
|
|
||||||
if rc == 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class RhsmPools(object):
|
|
||||||
"""
|
|
||||||
This class is used for manipulating pools subscriptions with RHSM
|
|
||||||
"""
|
|
||||||
def __init__(self, module):
|
|
||||||
self.module = module
|
|
||||||
self.products = self._load_product_list()
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self.products.__iter__()
|
|
||||||
|
|
||||||
def _load_product_list(self):
|
|
||||||
"""
|
|
||||||
Loads list of all available pools for system in data structure
|
|
||||||
"""
|
|
||||||
args = "subscription-manager list --available"
|
|
||||||
rc, stdout, stderr = self.module.run_command(args, check_rc=True)
|
|
||||||
|
|
||||||
products = []
|
|
||||||
for line in stdout.split('\n'):
|
|
||||||
# Remove leading+trailing whitespace
|
|
||||||
line = line.strip()
|
|
||||||
# An empty line implies the end of an output group
|
|
||||||
if len(line) == 0:
|
|
||||||
continue
|
|
||||||
# If a colon ':' is found, parse
|
|
||||||
elif ':' in line:
|
|
||||||
(key, value) = line.split(':',1)
|
|
||||||
key = key.strip().replace(" ", "") # To unify
|
|
||||||
value = value.strip()
|
|
||||||
if key in ['ProductName', 'SubscriptionName']:
|
|
||||||
# Remember the name for later processing
|
|
||||||
products.append(RhsmPool(self.module, _name=value, key=value))
|
|
||||||
elif products:
|
|
||||||
# Associate value with most recently recorded product
|
|
||||||
products[-1].__setattr__(key, value)
|
|
||||||
# FIXME - log some warning?
|
|
||||||
#else:
|
|
||||||
# warnings.warn("Unhandled subscription key/value: %s/%s" % (key,value))
|
|
||||||
return products
|
|
||||||
|
|
||||||
def filter(self, regexp='^$'):
|
|
||||||
'''
|
|
||||||
Return a list of RhsmPools whose name matches the provided regular expression
|
|
||||||
'''
|
|
||||||
r = re.compile(regexp)
|
|
||||||
for product in self.products:
|
|
||||||
if r.search(product._name):
|
|
||||||
yield product
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
|||||||
# (c) 2014 James Cammarata, <jcammarata@ansible.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/>.
|
|
||||||
|
|
||||||
def _get_quote_state(token, quote_char):
|
|
||||||
'''
|
|
||||||
the goal of this block is to determine if the quoted string
|
|
||||||
is unterminated in which case it needs to be put back together
|
|
||||||
'''
|
|
||||||
# the char before the current one, used to see if
|
|
||||||
# the current character is escaped
|
|
||||||
prev_char = None
|
|
||||||
for idx, cur_char in enumerate(token):
|
|
||||||
if idx > 0:
|
|
||||||
prev_char = token[idx-1]
|
|
||||||
if cur_char in '"\'' and prev_char != '\\':
|
|
||||||
if quote_char:
|
|
||||||
if cur_char == quote_char:
|
|
||||||
quote_char = None
|
|
||||||
else:
|
|
||||||
quote_char = cur_char
|
|
||||||
return quote_char
|
|
||||||
|
|
||||||
def _count_jinja2_blocks(token, cur_depth, open_token, close_token):
|
|
||||||
'''
|
|
||||||
this function counts the number of opening/closing blocks for a
|
|
||||||
given opening/closing type and adjusts the current depth for that
|
|
||||||
block based on the difference
|
|
||||||
'''
|
|
||||||
num_open = token.count(open_token)
|
|
||||||
num_close = token.count(close_token)
|
|
||||||
if num_open != num_close:
|
|
||||||
cur_depth += (num_open - num_close)
|
|
||||||
if cur_depth < 0:
|
|
||||||
cur_depth = 0
|
|
||||||
return cur_depth
|
|
||||||
|
|
||||||
def split_args(args):
|
|
||||||
'''
|
|
||||||
Splits args on whitespace, but intelligently reassembles
|
|
||||||
those that may have been split over a jinja2 block or quotes.
|
|
||||||
|
|
||||||
When used in a remote module, we won't ever have to be concerned about
|
|
||||||
jinja2 blocks, however this function is/will be used in the
|
|
||||||
core portions as well before the args are templated.
|
|
||||||
|
|
||||||
example input: a=b c="foo bar"
|
|
||||||
example output: ['a=b', 'c="foo bar"']
|
|
||||||
|
|
||||||
Basically this is a variation shlex that has some more intelligence for
|
|
||||||
how Ansible needs to use it.
|
|
||||||
'''
|
|
||||||
|
|
||||||
# the list of params parsed out of the arg string
|
|
||||||
# this is going to be the result value when we are donei
|
|
||||||
params = []
|
|
||||||
|
|
||||||
# here we encode the args, so we have a uniform charset to
|
|
||||||
# work with, and split on white space
|
|
||||||
args = args.strip()
|
|
||||||
try:
|
|
||||||
args = args.encode('utf-8')
|
|
||||||
do_decode = True
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
do_decode = False
|
|
||||||
items = args.split('\n')
|
|
||||||
|
|
||||||
# iterate over the tokens, and reassemble any that may have been
|
|
||||||
# split on a space inside a jinja2 block.
|
|
||||||
# ex if tokens are "{{", "foo", "}}" these go together
|
|
||||||
|
|
||||||
# These variables are used
|
|
||||||
# to keep track of the state of the parsing, since blocks and quotes
|
|
||||||
# may be nested within each other.
|
|
||||||
|
|
||||||
quote_char = None
|
|
||||||
inside_quotes = False
|
|
||||||
print_depth = 0 # used to count nested jinja2 {{ }} blocks
|
|
||||||
block_depth = 0 # used to count nested jinja2 {% %} blocks
|
|
||||||
comment_depth = 0 # used to count nested jinja2 {# #} blocks
|
|
||||||
|
|
||||||
# now we loop over each split chunk, coalescing tokens if the white space
|
|
||||||
# split occurred within quotes or a jinja2 block of some kind
|
|
||||||
for itemidx,item in enumerate(items):
|
|
||||||
|
|
||||||
# we split on spaces and newlines separately, so that we
|
|
||||||
# can tell which character we split on for reassembly
|
|
||||||
# inside quotation characters
|
|
||||||
tokens = item.strip().split(' ')
|
|
||||||
|
|
||||||
line_continuation = False
|
|
||||||
for idx,token in enumerate(tokens):
|
|
||||||
|
|
||||||
# if we hit a line continuation character, but
|
|
||||||
# we're not inside quotes, ignore it and continue
|
|
||||||
# on to the next token while setting a flag
|
|
||||||
if token == '\\' and not inside_quotes:
|
|
||||||
line_continuation = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# store the previous quoting state for checking later
|
|
||||||
was_inside_quotes = inside_quotes
|
|
||||||
quote_char = _get_quote_state(token, quote_char)
|
|
||||||
inside_quotes = quote_char is not None
|
|
||||||
|
|
||||||
# multiple conditions may append a token to the list of params,
|
|
||||||
# so we keep track with this flag to make sure it only happens once
|
|
||||||
# append means add to the end of the list, don't append means concatenate
|
|
||||||
# it to the end of the last token
|
|
||||||
appended = False
|
|
||||||
|
|
||||||
# if we're inside quotes now, but weren't before, append the token
|
|
||||||
# to the end of the list, since we'll tack on more to it later
|
|
||||||
# otherwise, if we're inside any jinja2 block, inside quotes, or we were
|
|
||||||
# inside quotes (but aren't now) concat this token to the last param
|
|
||||||
if inside_quotes and not was_inside_quotes:
|
|
||||||
params.append(token)
|
|
||||||
appended = True
|
|
||||||
elif print_depth or block_depth or comment_depth or inside_quotes or was_inside_quotes:
|
|
||||||
if idx == 0 and not inside_quotes and was_inside_quotes:
|
|
||||||
params[-1] = "%s%s" % (params[-1], token)
|
|
||||||
elif len(tokens) > 1:
|
|
||||||
spacer = ''
|
|
||||||
if idx > 0:
|
|
||||||
spacer = ' '
|
|
||||||
params[-1] = "%s%s%s" % (params[-1], spacer, token)
|
|
||||||
else:
|
|
||||||
spacer = ''
|
|
||||||
if not params[-1].endswith('\n') and idx == 0:
|
|
||||||
spacer = '\n'
|
|
||||||
params[-1] = "%s%s%s" % (params[-1], spacer, token)
|
|
||||||
appended = True
|
|
||||||
|
|
||||||
# if the number of paired block tags is not the same, the depth has changed, so we calculate that here
|
|
||||||
# and may append the current token to the params (if we haven't previously done so)
|
|
||||||
prev_print_depth = print_depth
|
|
||||||
print_depth = _count_jinja2_blocks(token, print_depth, "{{", "}}")
|
|
||||||
if print_depth != prev_print_depth and not appended:
|
|
||||||
params.append(token)
|
|
||||||
appended = True
|
|
||||||
|
|
||||||
prev_block_depth = block_depth
|
|
||||||
block_depth = _count_jinja2_blocks(token, block_depth, "{%", "%}")
|
|
||||||
if block_depth != prev_block_depth and not appended:
|
|
||||||
params.append(token)
|
|
||||||
appended = True
|
|
||||||
|
|
||||||
prev_comment_depth = comment_depth
|
|
||||||
comment_depth = _count_jinja2_blocks(token, comment_depth, "{#", "#}")
|
|
||||||
if comment_depth != prev_comment_depth and not appended:
|
|
||||||
params.append(token)
|
|
||||||
appended = True
|
|
||||||
|
|
||||||
# finally, if we're at zero depth for all blocks and not inside quotes, and have not
|
|
||||||
# yet appended anything to the list of params, we do so now
|
|
||||||
if not (print_depth or block_depth or comment_depth) and not inside_quotes and not appended and token != '':
|
|
||||||
params.append(token)
|
|
||||||
|
|
||||||
# if this was the last token in the list, and we have more than
|
|
||||||
# one item (meaning we split on newlines), add a newline back here
|
|
||||||
# to preserve the original structure
|
|
||||||
if len(items) > 1 and itemidx != len(items) - 1 and not line_continuation:
|
|
||||||
if not params[-1].endswith('\n') or item == '':
|
|
||||||
params[-1] += '\n'
|
|
||||||
|
|
||||||
# always clear the line continuation flag
|
|
||||||
line_continuation = False
|
|
||||||
|
|
||||||
# If we're done and things are not at zero depth or we're still inside quotes,
|
|
||||||
# raise an error to indicate that the args were unbalanced
|
|
||||||
if print_depth or block_depth or comment_depth or inside_quotes:
|
|
||||||
raise Exception("error while splitting arguments, either an unbalanced jinja2 block or quotes")
|
|
||||||
|
|
||||||
# finally, we decode each param back to the unicode it was in the arg string
|
|
||||||
if do_decode:
|
|
||||||
params = [x.decode('utf-8') for x in params]
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
def is_quoted(data):
|
|
||||||
return len(data) > 0 and (data[0] == '"' and data[-1] == '"' or data[0] == "'" and data[-1] == "'")
|
|
||||||
|
|
||||||
def unquote(data):
|
|
||||||
''' removes first and last quotes from a string, if the string starts and ends with the same quotes '''
|
|
||||||
if is_quoted(data):
|
|
||||||
return data[1:-1]
|
|
||||||
return data
|
|
||||||
|
|
@ -1,496 +0,0 @@
|
|||||||
# This code is part of Ansible, but is an independent component.
|
|
||||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
||||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
||||||
# still belong to the author of the module, and may assign their own license
|
|
||||||
# to the complete work.
|
|
||||||
#
|
|
||||||
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
# are permitted provided that the following conditions are met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
# this list of conditions and the following disclaimer in the documentation
|
|
||||||
# and/or other materials provided with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
||||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
||||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
||||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
||||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
try:
|
|
||||||
import urllib
|
|
||||||
HAS_URLLIB = True
|
|
||||||
except:
|
|
||||||
HAS_URLLIB = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import urllib2
|
|
||||||
HAS_URLLIB2 = True
|
|
||||||
except:
|
|
||||||
HAS_URLLIB2 = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import urlparse
|
|
||||||
HAS_URLPARSE = True
|
|
||||||
except:
|
|
||||||
HAS_URLPARSE = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ssl
|
|
||||||
HAS_SSL=True
|
|
||||||
except:
|
|
||||||
HAS_SSL=False
|
|
||||||
|
|
||||||
HAS_MATCH_HOSTNAME = True
|
|
||||||
try:
|
|
||||||
from ssl import match_hostname, CertificateError
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
from backports.ssl_match_hostname import match_hostname, CertificateError
|
|
||||||
except ImportError:
|
|
||||||
HAS_MATCH_HOSTNAME = False
|
|
||||||
|
|
||||||
import httplib
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
# This is a dummy cacert provided for Mac OS since you need at least 1
|
|
||||||
# ca cert, regardless of validity, for Python on Mac OS to use the
|
|
||||||
# keychain functionality in OpenSSL for validating SSL certificates.
|
|
||||||
# See: http://mercurial.selenic.com/wiki/CACertificates#Mac_OS_X_10.6_and_higher
|
|
||||||
DUMMY_CA_CERT = """-----BEGIN CERTIFICATE-----
|
|
||||||
MIICvDCCAiWgAwIBAgIJAO8E12S7/qEpMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
|
|
||||||
BAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEPMA0GA1UEBxMGRHVyaGFt
|
|
||||||
MRAwDgYDVQQKEwdBbnNpYmxlMB4XDTE0MDMxODIyMDAyMloXDTI0MDMxNTIyMDAy
|
|
||||||
MlowSTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMQ8wDQYD
|
|
||||||
VQQHEwZEdXJoYW0xEDAOBgNVBAoTB0Fuc2libGUwgZ8wDQYJKoZIhvcNAQEBBQAD
|
|
||||||
gY0AMIGJAoGBANtvpPq3IlNlRbCHhZAcP6WCzhc5RbsDqyh1zrkmLi0GwcQ3z/r9
|
|
||||||
gaWfQBYhHpobK2Tiq11TfraHeNB3/VfNImjZcGpN8Fl3MWwu7LfVkJy3gNNnxkA1
|
|
||||||
4Go0/LmIvRFHhbzgfuo9NFgjPmmab9eqXJceqZIlz2C8xA7EeG7ku0+vAgMBAAGj
|
|
||||||
gaswgagwHQYDVR0OBBYEFPnN1nPRqNDXGlCqCvdZchRNi/FaMHkGA1UdIwRyMHCA
|
|
||||||
FPnN1nPRqNDXGlCqCvdZchRNi/FaoU2kSzBJMQswCQYDVQQGEwJVUzEXMBUGA1UE
|
|
||||||
CBMOTm9ydGggQ2Fyb2xpbmExDzANBgNVBAcTBkR1cmhhbTEQMA4GA1UEChMHQW5z
|
|
||||||
aWJsZYIJAO8E12S7/qEpMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
|
|
||||||
MUB80IR6knq9K/tY+hvPsZer6eFMzO3JGkRFBh2kn6JdMDnhYGX7AXVHGflrwNQH
|
|
||||||
qFy+aenWXsC0ZvrikFxbQnX8GVtDADtVznxOi7XzFw7JOxdsVrpXgSN0eh0aMzvV
|
|
||||||
zKPZsZ2miVGclicJHzm5q080b1p/sZtuKIEZk6vZqEg=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
"""
|
|
||||||
|
|
||||||
class CustomHTTPSConnection(httplib.HTTPSConnection):
|
|
||||||
def connect(self):
|
|
||||||
"Connect to a host on a given (SSL) port."
|
|
||||||
|
|
||||||
if hasattr(self, 'source_address'):
|
|
||||||
sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address)
|
|
||||||
else:
|
|
||||||
sock = socket.create_connection((self.host, self.port), self.timeout)
|
|
||||||
if self._tunnel_host:
|
|
||||||
self.sock = sock
|
|
||||||
self._tunnel()
|
|
||||||
self.sock = ssl.wrap_socket(sock, keyfile=self.key_file, certfile=self.cert_file, ssl_version=ssl.PROTOCOL_TLSv1)
|
|
||||||
|
|
||||||
class CustomHTTPSHandler(urllib2.HTTPSHandler):
|
|
||||||
|
|
||||||
def https_open(self, req):
|
|
||||||
return self.do_open(CustomHTTPSConnection, req)
|
|
||||||
|
|
||||||
https_request = urllib2.AbstractHTTPHandler.do_request_
|
|
||||||
|
|
||||||
def generic_urlparse(parts):
|
|
||||||
'''
|
|
||||||
Returns a dictionary of url parts as parsed by urlparse,
|
|
||||||
but accounts for the fact that older versions of that
|
|
||||||
library do not support named attributes (ie. .netloc)
|
|
||||||
'''
|
|
||||||
generic_parts = dict()
|
|
||||||
if hasattr(parts, 'netloc'):
|
|
||||||
# urlparse is newer, just read the fields straight
|
|
||||||
# from the parts object
|
|
||||||
generic_parts['scheme'] = parts.scheme
|
|
||||||
generic_parts['netloc'] = parts.netloc
|
|
||||||
generic_parts['path'] = parts.path
|
|
||||||
generic_parts['params'] = parts.params
|
|
||||||
generic_parts['query'] = parts.query
|
|
||||||
generic_parts['fragment'] = parts.fragment
|
|
||||||
generic_parts['username'] = parts.username
|
|
||||||
generic_parts['password'] = parts.password
|
|
||||||
generic_parts['hostname'] = parts.hostname
|
|
||||||
generic_parts['port'] = parts.port
|
|
||||||
else:
|
|
||||||
# we have to use indexes, and then parse out
|
|
||||||
# the other parts not supported by indexing
|
|
||||||
generic_parts['scheme'] = parts[0]
|
|
||||||
generic_parts['netloc'] = parts[1]
|
|
||||||
generic_parts['path'] = parts[2]
|
|
||||||
generic_parts['params'] = parts[3]
|
|
||||||
generic_parts['query'] = parts[4]
|
|
||||||
generic_parts['fragment'] = parts[5]
|
|
||||||
# get the username, password, etc.
|
|
||||||
try:
|
|
||||||
netloc_re = re.compile(r'^((?:\w)+(?::(?:\w)+)?@)?([A-Za-z0-9.-]+)(:\d+)?$')
|
|
||||||
(auth, hostname, port) = netloc_re.match(parts[1])
|
|
||||||
if port:
|
|
||||||
# the capture group for the port will include the ':',
|
|
||||||
# so remove it and convert the port to an integer
|
|
||||||
port = int(port[1:])
|
|
||||||
if auth:
|
|
||||||
# the capture group above inclues the @, so remove it
|
|
||||||
# and then split it up based on the first ':' found
|
|
||||||
auth = auth[:-1]
|
|
||||||
username, password = auth.split(':', 1)
|
|
||||||
generic_parts['username'] = username
|
|
||||||
generic_parts['password'] = password
|
|
||||||
generic_parts['hostname'] = hostnme
|
|
||||||
generic_parts['port'] = port
|
|
||||||
except:
|
|
||||||
generic_parts['username'] = None
|
|
||||||
generic_parts['password'] = None
|
|
||||||
generic_parts['hostname'] = None
|
|
||||||
generic_parts['port'] = None
|
|
||||||
return generic_parts
|
|
||||||
|
|
||||||
class RequestWithMethod(urllib2.Request):
|
|
||||||
'''
|
|
||||||
Workaround for using DELETE/PUT/etc with urllib2
|
|
||||||
Originally contained in library/net_infrastructure/dnsmadeeasy
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, url, method, data=None, headers={}):
|
|
||||||
self._method = method
|
|
||||||
urllib2.Request.__init__(self, url, data, headers)
|
|
||||||
|
|
||||||
def get_method(self):
|
|
||||||
if self._method:
|
|
||||||
return self._method
|
|
||||||
else:
|
|
||||||
return urllib2.Request.get_method(self)
|
|
||||||
|
|
||||||
|
|
||||||
class SSLValidationHandler(urllib2.BaseHandler):
|
|
||||||
'''
|
|
||||||
A custom handler class for SSL validation.
|
|
||||||
|
|
||||||
Based on:
|
|
||||||
http://stackoverflow.com/questions/1087227/validate-ssl-certificates-with-python
|
|
||||||
http://techknack.net/python-urllib2-handlers/
|
|
||||||
'''
|
|
||||||
CONNECT_COMMAND = "CONNECT %s:%s HTTP/1.0\r\nConnection: close\r\n"
|
|
||||||
|
|
||||||
def __init__(self, module, hostname, port):
|
|
||||||
self.module = module
|
|
||||||
self.hostname = hostname
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
def get_ca_certs(self):
|
|
||||||
# tries to find a valid CA cert in one of the
|
|
||||||
# standard locations for the current distribution
|
|
||||||
|
|
||||||
ca_certs = []
|
|
||||||
paths_checked = []
|
|
||||||
platform = get_platform()
|
|
||||||
distribution = get_distribution()
|
|
||||||
|
|
||||||
# build a list of paths to check for .crt/.pem files
|
|
||||||
# based on the platform type
|
|
||||||
paths_checked.append('/etc/ssl/certs')
|
|
||||||
if platform == 'Linux':
|
|
||||||
paths_checked.append('/etc/pki/ca-trust/extracted/pem')
|
|
||||||
paths_checked.append('/etc/pki/tls/certs')
|
|
||||||
paths_checked.append('/usr/share/ca-certificates/cacert.org')
|
|
||||||
elif platform == 'FreeBSD':
|
|
||||||
paths_checked.append('/usr/local/share/certs')
|
|
||||||
elif platform == 'OpenBSD':
|
|
||||||
paths_checked.append('/etc/ssl')
|
|
||||||
elif platform == 'NetBSD':
|
|
||||||
ca_certs.append('/etc/openssl/certs')
|
|
||||||
elif platform == 'SunOS':
|
|
||||||
paths_checked.append('/opt/local/etc/openssl/certs')
|
|
||||||
|
|
||||||
# fall back to a user-deployed cert in a standard
|
|
||||||
# location if the OS platform one is not available
|
|
||||||
paths_checked.append('/etc/ansible')
|
|
||||||
|
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp()
|
|
||||||
|
|
||||||
# Write the dummy ca cert if we are running on Mac OS X
|
|
||||||
if platform == 'Darwin':
|
|
||||||
os.write(tmp_fd, DUMMY_CA_CERT)
|
|
||||||
# Default Homebrew path for OpenSSL certs
|
|
||||||
paths_checked.append('/usr/local/etc/openssl')
|
|
||||||
|
|
||||||
# for all of the paths, find any .crt or .pem files
|
|
||||||
# and compile them into single temp file for use
|
|
||||||
# in the ssl check to speed up the test
|
|
||||||
for path in paths_checked:
|
|
||||||
if os.path.exists(path) and os.path.isdir(path):
|
|
||||||
dir_contents = os.listdir(path)
|
|
||||||
for f in dir_contents:
|
|
||||||
full_path = os.path.join(path, f)
|
|
||||||
if os.path.isfile(full_path) and os.path.splitext(f)[1] in ('.crt','.pem'):
|
|
||||||
try:
|
|
||||||
cert_file = open(full_path, 'r')
|
|
||||||
os.write(tmp_fd, cert_file.read())
|
|
||||||
os.write(tmp_fd, '\n')
|
|
||||||
cert_file.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return (tmp_path, paths_checked)
|
|
||||||
|
|
||||||
def validate_proxy_response(self, response, valid_codes=[200]):
|
|
||||||
'''
|
|
||||||
make sure we get back a valid code from the proxy
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
(http_version, resp_code, msg) = re.match(r'(HTTP/\d\.\d) (\d\d\d) (.*)', response).groups()
|
|
||||||
if int(resp_code) not in valid_codes:
|
|
||||||
raise Exception
|
|
||||||
except:
|
|
||||||
self.module.fail_json(msg='Connection to proxy failed')
|
|
||||||
|
|
||||||
def detect_no_proxy(self, url):
|
|
||||||
'''
|
|
||||||
Detect if the 'no_proxy' environment variable is set and honor those locations.
|
|
||||||
'''
|
|
||||||
env_no_proxy = os.environ.get('no_proxy')
|
|
||||||
if env_no_proxy:
|
|
||||||
env_no_proxy = env_no_proxy.split(',')
|
|
||||||
netloc = urlparse.urlparse(url).netloc
|
|
||||||
|
|
||||||
for host in env_no_proxy:
|
|
||||||
if netloc.endswith(host) or netloc.split(':')[0].endswith(host):
|
|
||||||
# Our requested URL matches something in no_proxy, so don't
|
|
||||||
# use the proxy for this
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def http_request(self, req):
|
|
||||||
tmp_ca_cert_path, paths_checked = self.get_ca_certs()
|
|
||||||
https_proxy = os.environ.get('https_proxy')
|
|
||||||
|
|
||||||
# Detect if 'no_proxy' environment variable is set and if our URL is included
|
|
||||||
use_proxy = self.detect_no_proxy(req.get_full_url())
|
|
||||||
|
|
||||||
if not use_proxy:
|
|
||||||
# ignore proxy settings for this host request
|
|
||||||
return req
|
|
||||||
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
if https_proxy:
|
|
||||||
proxy_parts = generic_urlparse(urlparse.urlparse(https_proxy))
|
|
||||||
s.connect((proxy_parts.get('hostname'), proxy_parts.get('port')))
|
|
||||||
if proxy_parts.get('scheme') == 'http':
|
|
||||||
s.sendall(self.CONNECT_COMMAND % (self.hostname, self.port))
|
|
||||||
if proxy_parts.get('username'):
|
|
||||||
credentials = "%s:%s" % (proxy_parts.get('username',''), proxy_parts.get('password',''))
|
|
||||||
s.sendall('Proxy-Authorization: Basic %s\r\n' % credentials.encode('base64').strip())
|
|
||||||
s.sendall('\r\n')
|
|
||||||
connect_result = s.recv(4096)
|
|
||||||
self.validate_proxy_response(connect_result)
|
|
||||||
ssl_s = ssl.wrap_socket(s, ca_certs=tmp_ca_cert_path, cert_reqs=ssl.CERT_REQUIRED)
|
|
||||||
match_hostname(ssl_s.getpeercert(), self.hostname)
|
|
||||||
else:
|
|
||||||
self.module.fail_json(msg='Unsupported proxy scheme: %s. Currently ansible only supports HTTP proxies.' % proxy_parts.get('scheme'))
|
|
||||||
else:
|
|
||||||
s.connect((self.hostname, self.port))
|
|
||||||
ssl_s = ssl.wrap_socket(s, ca_certs=tmp_ca_cert_path, cert_reqs=ssl.CERT_REQUIRED)
|
|
||||||
match_hostname(ssl_s.getpeercert(), self.hostname)
|
|
||||||
# close the ssl connection
|
|
||||||
#ssl_s.unwrap()
|
|
||||||
s.close()
|
|
||||||
except (ssl.SSLError, socket.error), e:
|
|
||||||
# fail if we tried all of the certs but none worked
|
|
||||||
if 'connection refused' in str(e).lower():
|
|
||||||
self.module.fail_json(msg='Failed to connect to %s:%s.' % (self.hostname, self.port))
|
|
||||||
else:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg='Failed to validate the SSL certificate for %s:%s. ' % (self.hostname, self.port) + \
|
|
||||||
'Use validate_certs=no or make sure your managed systems have a valid CA certificate installed. ' + \
|
|
||||||
'Paths checked for this platform: %s' % ", ".join(paths_checked)
|
|
||||||
)
|
|
||||||
except CertificateError:
|
|
||||||
self.module.fail_json(msg="SSL Certificate does not belong to %s. Make sure the url has a certificate that belongs to it or use validate_certs=no (insecure)" % self.hostname)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# cleanup the temp file created, don't worry
|
|
||||||
# if it fails for some reason
|
|
||||||
os.remove(tmp_ca_cert_path)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return req
|
|
||||||
|
|
||||||
https_request = http_request
|
|
||||||
|
|
||||||
|
|
||||||
def url_argument_spec():
|
|
||||||
'''
|
|
||||||
Creates an argument spec that can be used with any module
|
|
||||||
that will be requesting content via urllib/urllib2
|
|
||||||
'''
|
|
||||||
return dict(
|
|
||||||
url = dict(),
|
|
||||||
force = dict(default='no', aliases=['thirsty'], type='bool'),
|
|
||||||
http_agent = dict(default='ansible-httpget'),
|
|
||||||
use_proxy = dict(default='yes', type='bool'),
|
|
||||||
validate_certs = dict(default='yes', type='bool'),
|
|
||||||
url_username = dict(required=False),
|
|
||||||
url_password = dict(required=False),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_url(module, url, data=None, headers=None, method=None,
|
|
||||||
use_proxy=True, force=False, last_mod_time=None, timeout=10):
|
|
||||||
'''
|
|
||||||
Fetches a file from an HTTP/FTP server using urllib2
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not HAS_URLLIB:
|
|
||||||
module.fail_json(msg='urllib is not installed')
|
|
||||||
if not HAS_URLLIB2:
|
|
||||||
module.fail_json(msg='urllib2 is not installed')
|
|
||||||
elif not HAS_URLPARSE:
|
|
||||||
module.fail_json(msg='urlparse is not installed')
|
|
||||||
|
|
||||||
r = None
|
|
||||||
handlers = []
|
|
||||||
info = dict(url=url)
|
|
||||||
|
|
||||||
distribution = get_distribution()
|
|
||||||
# Get validate_certs from the module params
|
|
||||||
validate_certs = module.params.get('validate_certs', True)
|
|
||||||
|
|
||||||
# FIXME: change the following to use the generic_urlparse function
|
|
||||||
# to remove the indexed references for 'parsed'
|
|
||||||
parsed = urlparse.urlparse(url)
|
|
||||||
if parsed[0] == 'https' and validate_certs:
|
|
||||||
if not HAS_SSL:
|
|
||||||
if distribution == 'Redhat':
|
|
||||||
module.fail_json(msg='SSL validation is not available in your version of python. You can use validate_certs=no, however this is unsafe and not recommended. You can also install python-ssl from EPEL')
|
|
||||||
else:
|
|
||||||
module.fail_json(msg='SSL validation is not available in your version of python. You can use validate_certs=no, however this is unsafe and not recommended')
|
|
||||||
if not HAS_MATCH_HOSTNAME:
|
|
||||||
module.fail_json(msg='Available SSL validation does not check that the certificate matches the hostname. You can install backports.ssl_match_hostname or update your managed machine to python-2.7.9 or newer. You could also use validate_certs=no, however this is unsafe and not recommended')
|
|
||||||
|
|
||||||
# do the cert validation
|
|
||||||
netloc = parsed[1]
|
|
||||||
if '@' in netloc:
|
|
||||||
netloc = netloc.split('@', 1)[1]
|
|
||||||
if ':' in netloc:
|
|
||||||
hostname, port = netloc.split(':', 1)
|
|
||||||
port = int(port)
|
|
||||||
else:
|
|
||||||
hostname = netloc
|
|
||||||
port = 443
|
|
||||||
# create the SSL validation handler and
|
|
||||||
# add it to the list of handlers
|
|
||||||
ssl_handler = SSLValidationHandler(module, hostname, port)
|
|
||||||
handlers.append(ssl_handler)
|
|
||||||
|
|
||||||
if parsed[0] != 'ftp':
|
|
||||||
username = module.params.get('url_username', '')
|
|
||||||
if username:
|
|
||||||
password = module.params.get('url_password', '')
|
|
||||||
netloc = parsed[1]
|
|
||||||
elif '@' in parsed[1]:
|
|
||||||
credentials, netloc = parsed[1].split('@', 1)
|
|
||||||
if ':' in credentials:
|
|
||||||
username, password = credentials.split(':', 1)
|
|
||||||
else:
|
|
||||||
username = credentials
|
|
||||||
password = ''
|
|
||||||
|
|
||||||
parsed = list(parsed)
|
|
||||||
parsed[1] = netloc
|
|
||||||
|
|
||||||
# reconstruct url without credentials
|
|
||||||
url = urlparse.urlunparse(parsed)
|
|
||||||
|
|
||||||
if username:
|
|
||||||
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
|
||||||
|
|
||||||
# this creates a password manager
|
|
||||||
passman.add_password(None, netloc, username, password)
|
|
||||||
|
|
||||||
# because we have put None at the start it will always
|
|
||||||
# use this username/password combination for urls
|
|
||||||
# for which `theurl` is a super-url
|
|
||||||
authhandler = urllib2.HTTPBasicAuthHandler(passman)
|
|
||||||
|
|
||||||
# create the AuthHandler
|
|
||||||
handlers.append(authhandler)
|
|
||||||
|
|
||||||
if not use_proxy:
|
|
||||||
proxyhandler = urllib2.ProxyHandler({})
|
|
||||||
handlers.append(proxyhandler)
|
|
||||||
|
|
||||||
# pre-2.6 versions of python cannot use the custom https
|
|
||||||
# handler, since the socket class is lacking this method
|
|
||||||
if hasattr(socket, 'create_connection'):
|
|
||||||
handlers.append(CustomHTTPSHandler)
|
|
||||||
|
|
||||||
opener = urllib2.build_opener(*handlers)
|
|
||||||
urllib2.install_opener(opener)
|
|
||||||
|
|
||||||
if method:
|
|
||||||
if method.upper() not in ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT'):
|
|
||||||
module.fail_json(msg='invalid HTTP request method; %s' % method.upper())
|
|
||||||
request = RequestWithMethod(url, method.upper(), data)
|
|
||||||
else:
|
|
||||||
request = urllib2.Request(url, data)
|
|
||||||
|
|
||||||
# add the custom agent header, to help prevent issues
|
|
||||||
# with sites that block the default urllib agent string
|
|
||||||
request.add_header('User-agent', module.params.get('http_agent'))
|
|
||||||
|
|
||||||
# if we're ok with getting a 304, set the timestamp in the
|
|
||||||
# header, otherwise make sure we don't get a cached copy
|
|
||||||
if last_mod_time and not force:
|
|
||||||
tstamp = last_mod_time.strftime('%a, %d %b %Y %H:%M:%S +0000')
|
|
||||||
request.add_header('If-Modified-Since', tstamp)
|
|
||||||
else:
|
|
||||||
request.add_header('cache-control', 'no-cache')
|
|
||||||
|
|
||||||
# user defined headers now, which may override things we've set above
|
|
||||||
if headers:
|
|
||||||
if not isinstance(headers, dict):
|
|
||||||
module.fail_json("headers provided to fetch_url() must be a dict")
|
|
||||||
for header in headers:
|
|
||||||
request.add_header(header, headers[header])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if sys.version_info < (2,6,0):
|
|
||||||
# urlopen in python prior to 2.6.0 did not
|
|
||||||
# have a timeout parameter
|
|
||||||
r = urllib2.urlopen(request, None)
|
|
||||||
else:
|
|
||||||
r = urllib2.urlopen(request, None, timeout)
|
|
||||||
info.update(r.info())
|
|
||||||
info['url'] = r.geturl() # The URL goes in too, because of redirects.
|
|
||||||
info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), status=200))
|
|
||||||
except urllib2.HTTPError, e:
|
|
||||||
info.update(dict(msg=str(e), status=e.code))
|
|
||||||
except urllib2.URLError, e:
|
|
||||||
code = int(getattr(e, 'code', -1))
|
|
||||||
info.update(dict(msg="Request failed: %s" % str(e), status=code))
|
|
||||||
except socket.error, e:
|
|
||||||
info.update(dict(msg="Connection failure: %s" % str(e), status=-1))
|
|
||||||
except Exception, e:
|
|
||||||
info.update(dict(msg="An unknown error occurred: %s" % str(e), status=-1))
|
|
||||||
|
|
||||||
return r, info
|
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
Subproject commit f8d8af17cdc72500af8319c96004b86ac702a0a4
|
|
@ -1 +0,0 @@
|
|||||||
Subproject commit 495ad450e53feb1cd26218dc68056cc34d1ea9ff
|
|
@ -1,874 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import ansible.inventory
|
|
||||||
import ansible.constants as C
|
|
||||||
import ansible.runner
|
|
||||||
from ansible.utils.template import template
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.module_utils.splitter import split_args, unquote
|
|
||||||
import ansible.callbacks
|
|
||||||
import ansible.cache
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import collections
|
|
||||||
from play import Play
|
|
||||||
import StringIO
|
|
||||||
import pipes
|
|
||||||
|
|
||||||
# the setup cache stores all variables about a host
|
|
||||||
# gathered during the setup step, while the vars cache
|
|
||||||
# holds all other variables about a host
|
|
||||||
SETUP_CACHE = ansible.cache.FactCache()
|
|
||||||
VARS_CACHE = collections.defaultdict(dict)
|
|
||||||
RESERVED_TAGS = ['all','tagged','untagged','always']
|
|
||||||
|
|
||||||
|
|
||||||
class PlayBook(object):
|
|
||||||
'''
|
|
||||||
runs an ansible playbook, given as a datastructure or YAML filename.
|
|
||||||
A playbook is a deployment, config management, or automation based
|
|
||||||
set of commands to run in series.
|
|
||||||
|
|
||||||
multiple plays/tasks do not execute simultaneously, but tasks in each
|
|
||||||
pattern do execute in parallel (according to the number of forks
|
|
||||||
requested) among the hosts they address
|
|
||||||
'''
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
playbook = None,
|
|
||||||
host_list = C.DEFAULT_HOST_LIST,
|
|
||||||
module_path = None,
|
|
||||||
forks = C.DEFAULT_FORKS,
|
|
||||||
timeout = C.DEFAULT_TIMEOUT,
|
|
||||||
remote_user = C.DEFAULT_REMOTE_USER,
|
|
||||||
remote_pass = C.DEFAULT_REMOTE_PASS,
|
|
||||||
remote_port = None,
|
|
||||||
transport = C.DEFAULT_TRANSPORT,
|
|
||||||
private_key_file = C.DEFAULT_PRIVATE_KEY_FILE,
|
|
||||||
callbacks = None,
|
|
||||||
runner_callbacks = None,
|
|
||||||
stats = None,
|
|
||||||
extra_vars = None,
|
|
||||||
only_tags = None,
|
|
||||||
skip_tags = None,
|
|
||||||
subset = C.DEFAULT_SUBSET,
|
|
||||||
inventory = None,
|
|
||||||
check = False,
|
|
||||||
diff = False,
|
|
||||||
any_errors_fatal = False,
|
|
||||||
vault_password = False,
|
|
||||||
force_handlers = False,
|
|
||||||
# privilege escalation
|
|
||||||
become = C.DEFAULT_BECOME,
|
|
||||||
become_method = C.DEFAULT_BECOME_METHOD,
|
|
||||||
become_user = C.DEFAULT_BECOME_USER,
|
|
||||||
become_pass = None,
|
|
||||||
):
|
|
||||||
|
|
||||||
"""
|
|
||||||
playbook: path to a playbook file
|
|
||||||
host_list: path to a file like /etc/ansible/hosts
|
|
||||||
module_path: path to ansible modules, like /usr/share/ansible/
|
|
||||||
forks: desired level of parallelism
|
|
||||||
timeout: connection timeout
|
|
||||||
remote_user: run as this user if not specified in a particular play
|
|
||||||
remote_pass: use this remote password (for all plays) vs using SSH keys
|
|
||||||
remote_port: default remote port to use if not specified with the host or play
|
|
||||||
transport: how to connect to hosts that don't specify a transport (local, paramiko, etc)
|
|
||||||
callbacks output callbacks for the playbook
|
|
||||||
runner_callbacks: more callbacks, this time for the runner API
|
|
||||||
stats: holds aggregrate data about events occurring to each host
|
|
||||||
inventory: can be specified instead of host_list to use a pre-existing inventory object
|
|
||||||
check: don't change anything, just try to detect some potential changes
|
|
||||||
any_errors_fatal: terminate the entire execution immediately when one of the hosts has failed
|
|
||||||
force_handlers: continue to notify and run handlers even if a task fails
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.SETUP_CACHE = SETUP_CACHE
|
|
||||||
self.VARS_CACHE = VARS_CACHE
|
|
||||||
|
|
||||||
arguments = []
|
|
||||||
if playbook is None:
|
|
||||||
arguments.append('playbook')
|
|
||||||
if callbacks is None:
|
|
||||||
arguments.append('callbacks')
|
|
||||||
if runner_callbacks is None:
|
|
||||||
arguments.append('runner_callbacks')
|
|
||||||
if stats is None:
|
|
||||||
arguments.append('stats')
|
|
||||||
if arguments:
|
|
||||||
raise Exception('PlayBook missing required arguments: %s' % ', '.join(arguments))
|
|
||||||
|
|
||||||
if extra_vars is None:
|
|
||||||
extra_vars = {}
|
|
||||||
if only_tags is None:
|
|
||||||
only_tags = [ 'all' ]
|
|
||||||
if skip_tags is None:
|
|
||||||
skip_tags = []
|
|
||||||
|
|
||||||
self.check = check
|
|
||||||
self.diff = diff
|
|
||||||
self.module_path = module_path
|
|
||||||
self.forks = forks
|
|
||||||
self.timeout = timeout
|
|
||||||
self.remote_user = remote_user
|
|
||||||
self.remote_pass = remote_pass
|
|
||||||
self.remote_port = remote_port
|
|
||||||
self.transport = transport
|
|
||||||
self.callbacks = callbacks
|
|
||||||
self.runner_callbacks = runner_callbacks
|
|
||||||
self.stats = stats
|
|
||||||
self.extra_vars = extra_vars
|
|
||||||
self.global_vars = {}
|
|
||||||
self.private_key_file = private_key_file
|
|
||||||
self.only_tags = only_tags
|
|
||||||
self.skip_tags = skip_tags
|
|
||||||
self.any_errors_fatal = any_errors_fatal
|
|
||||||
self.vault_password = vault_password
|
|
||||||
self.force_handlers = force_handlers
|
|
||||||
|
|
||||||
self.become = become
|
|
||||||
self.become_method = become_method
|
|
||||||
self.become_user = become_user
|
|
||||||
self.become_pass = become_pass
|
|
||||||
|
|
||||||
self.callbacks.playbook = self
|
|
||||||
self.runner_callbacks.playbook = self
|
|
||||||
|
|
||||||
if inventory is None:
|
|
||||||
self.inventory = ansible.inventory.Inventory(host_list)
|
|
||||||
self.inventory.subset(subset)
|
|
||||||
else:
|
|
||||||
self.inventory = inventory
|
|
||||||
|
|
||||||
if self.module_path is not None:
|
|
||||||
utils.plugins.module_finder.add_directory(self.module_path)
|
|
||||||
|
|
||||||
self.basedir = os.path.dirname(playbook) or '.'
|
|
||||||
utils.plugins.push_basedir(self.basedir)
|
|
||||||
|
|
||||||
# let inventory know the playbook basedir so it can load more vars
|
|
||||||
self.inventory.set_playbook_basedir(self.basedir)
|
|
||||||
|
|
||||||
vars = extra_vars.copy()
|
|
||||||
vars['playbook_dir'] = os.path.abspath(self.basedir)
|
|
||||||
if self.inventory.basedir() is not None:
|
|
||||||
vars['inventory_dir'] = self.inventory.basedir()
|
|
||||||
|
|
||||||
if self.inventory.src() is not None:
|
|
||||||
vars['inventory_file'] = self.inventory.src()
|
|
||||||
|
|
||||||
self.filename = playbook
|
|
||||||
(self.playbook, self.play_basedirs) = self._load_playbook_from_file(playbook, vars)
|
|
||||||
ansible.callbacks.load_callback_plugins()
|
|
||||||
ansible.callbacks.set_playbook(self.callbacks, self)
|
|
||||||
|
|
||||||
self._ansible_version = utils.version_info(gitinfo=True)
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _get_playbook_vars(self, play_ds, existing_vars):
|
|
||||||
'''
|
|
||||||
Gets the vars specified with the play and blends them
|
|
||||||
with any existing vars that have already been read in
|
|
||||||
'''
|
|
||||||
new_vars = existing_vars.copy()
|
|
||||||
if 'vars' in play_ds:
|
|
||||||
if isinstance(play_ds['vars'], dict):
|
|
||||||
new_vars.update(play_ds['vars'])
|
|
||||||
elif isinstance(play_ds['vars'], list):
|
|
||||||
for v in play_ds['vars']:
|
|
||||||
new_vars.update(v)
|
|
||||||
return new_vars
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _get_include_info(self, play_ds, basedir, existing_vars={}):
|
|
||||||
'''
|
|
||||||
Gets any key=value pairs specified with the included file
|
|
||||||
name and returns the merged vars along with the path
|
|
||||||
'''
|
|
||||||
new_vars = existing_vars.copy()
|
|
||||||
tokens = split_args(play_ds.get('include', ''))
|
|
||||||
for t in tokens[1:]:
|
|
||||||
try:
|
|
||||||
(k,v) = unquote(t).split("=", 1)
|
|
||||||
new_vars[k] = template(basedir, v, new_vars)
|
|
||||||
except ValueError, e:
|
|
||||||
raise errors.AnsibleError('included playbook variables must be in the form k=v, got: %s' % t)
|
|
||||||
|
|
||||||
return (new_vars, unquote(tokens[0]))
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _get_playbook_vars_files(self, play_ds, existing_vars_files):
|
|
||||||
new_vars_files = list(existing_vars_files)
|
|
||||||
if 'vars_files' in play_ds:
|
|
||||||
new_vars_files = utils.list_union(new_vars_files, play_ds['vars_files'])
|
|
||||||
return new_vars_files
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _extend_play_vars(self, play, vars={}):
|
|
||||||
'''
|
|
||||||
Extends the given play's variables with the additional specified vars.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if 'vars' not in play or not play['vars']:
|
|
||||||
# someone left out or put an empty "vars:" entry in their playbook
|
|
||||||
return vars.copy()
|
|
||||||
|
|
||||||
play_vars = None
|
|
||||||
if isinstance(play['vars'], dict):
|
|
||||||
play_vars = play['vars'].copy()
|
|
||||||
play_vars.update(vars)
|
|
||||||
elif isinstance(play['vars'], list):
|
|
||||||
# nobody should really do this, but handle vars: a=1 b=2
|
|
||||||
play_vars = play['vars'][:]
|
|
||||||
play_vars.extend([{k:v} for k,v in vars.iteritems()])
|
|
||||||
|
|
||||||
return play_vars
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _load_playbook_from_file(self, path, vars={}, vars_files=[]):
|
|
||||||
'''
|
|
||||||
run top level error checking on playbooks and allow them to include other playbooks.
|
|
||||||
'''
|
|
||||||
|
|
||||||
playbook_data = utils.parse_yaml_from_file(path, vault_password=self.vault_password)
|
|
||||||
accumulated_plays = []
|
|
||||||
play_basedirs = []
|
|
||||||
|
|
||||||
if type(playbook_data) != list:
|
|
||||||
raise errors.AnsibleError("parse error: playbooks must be formatted as a YAML list, got %s" % type(playbook_data))
|
|
||||||
|
|
||||||
basedir = os.path.dirname(path) or '.'
|
|
||||||
utils.plugins.push_basedir(basedir)
|
|
||||||
for play in playbook_data:
|
|
||||||
if type(play) != dict:
|
|
||||||
raise errors.AnsibleError("parse error: each play in a playbook must be a YAML dictionary (hash), received: %s" % play)
|
|
||||||
|
|
||||||
if 'include' in play:
|
|
||||||
# a playbook (list of plays) decided to include some other list of plays
|
|
||||||
# from another file. The result is a flat list of plays in the end.
|
|
||||||
|
|
||||||
play_vars = self._get_playbook_vars(play, vars)
|
|
||||||
play_vars_files = self._get_playbook_vars_files(play, vars_files)
|
|
||||||
inc_vars, inc_path = self._get_include_info(play, basedir, play_vars)
|
|
||||||
play_vars.update(inc_vars)
|
|
||||||
|
|
||||||
included_path = utils.path_dwim(basedir, template(basedir, inc_path, play_vars))
|
|
||||||
(plays, basedirs) = self._load_playbook_from_file(included_path, vars=play_vars, vars_files=play_vars_files)
|
|
||||||
for p in plays:
|
|
||||||
# support for parameterized play includes works by passing
|
|
||||||
# those variables along to the subservient play
|
|
||||||
p['vars'] = self._extend_play_vars(p, play_vars)
|
|
||||||
# now add in the vars_files
|
|
||||||
p['vars_files'] = utils.list_union(p.get('vars_files', []), play_vars_files)
|
|
||||||
|
|
||||||
accumulated_plays.extend(plays)
|
|
||||||
play_basedirs.extend(basedirs)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
# this is a normal (non-included play)
|
|
||||||
accumulated_plays.append(play)
|
|
||||||
play_basedirs.append(basedir)
|
|
||||||
|
|
||||||
return (accumulated_plays, play_basedirs)
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
''' run all patterns in the playbook '''
|
|
||||||
plays = []
|
|
||||||
matched_tags_all = set()
|
|
||||||
unmatched_tags_all = set()
|
|
||||||
|
|
||||||
# loop through all patterns and run them
|
|
||||||
self.callbacks.on_start()
|
|
||||||
for (play_ds, play_basedir) in zip(self.playbook, self.play_basedirs):
|
|
||||||
play = Play(self, play_ds, play_basedir, vault_password=self.vault_password)
|
|
||||||
assert play is not None
|
|
||||||
|
|
||||||
matched_tags, unmatched_tags = play.compare_tags(self.only_tags)
|
|
||||||
|
|
||||||
matched_tags_all = matched_tags_all | matched_tags
|
|
||||||
unmatched_tags_all = unmatched_tags_all | unmatched_tags
|
|
||||||
|
|
||||||
# Remove tasks we wish to skip
|
|
||||||
matched_tags = matched_tags - set(self.skip_tags)
|
|
||||||
|
|
||||||
# if we have matched_tags, the play must be run.
|
|
||||||
# if the play contains no tasks, assume we just want to gather facts
|
|
||||||
# in this case there are actually 3 meta tasks (handler flushes) not 0
|
|
||||||
# tasks, so that's why there's a check against 3
|
|
||||||
if (len(matched_tags) > 0 or len(play.tasks()) == 3):
|
|
||||||
plays.append(play)
|
|
||||||
|
|
||||||
# if the playbook is invoked with --tags or --skip-tags that don't
|
|
||||||
# exist at all in the playbooks then we need to raise an error so that
|
|
||||||
# the user can correct the arguments.
|
|
||||||
unknown_tags = ((set(self.only_tags) | set(self.skip_tags)) -
|
|
||||||
(matched_tags_all | unmatched_tags_all))
|
|
||||||
|
|
||||||
for t in RESERVED_TAGS:
|
|
||||||
unknown_tags.discard(t)
|
|
||||||
|
|
||||||
if len(unknown_tags) > 0:
|
|
||||||
for t in RESERVED_TAGS:
|
|
||||||
unmatched_tags_all.discard(t)
|
|
||||||
msg = 'tag(s) not found in playbook: %s. possible values: %s'
|
|
||||||
unknown = ','.join(sorted(unknown_tags))
|
|
||||||
unmatched = ','.join(sorted(unmatched_tags_all))
|
|
||||||
raise errors.AnsibleError(msg % (unknown, unmatched))
|
|
||||||
|
|
||||||
for play in plays:
|
|
||||||
ansible.callbacks.set_play(self.callbacks, play)
|
|
||||||
ansible.callbacks.set_play(self.runner_callbacks, play)
|
|
||||||
if not self._run_play(play):
|
|
||||||
break
|
|
||||||
|
|
||||||
ansible.callbacks.set_play(self.callbacks, None)
|
|
||||||
ansible.callbacks.set_play(self.runner_callbacks, None)
|
|
||||||
|
|
||||||
# summarize the results
|
|
||||||
results = {}
|
|
||||||
for host in self.stats.processed.keys():
|
|
||||||
results[host] = self.stats.summarize(host)
|
|
||||||
return results
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _async_poll(self, poller, async_seconds, async_poll_interval):
|
|
||||||
''' launch an async job, if poll_interval is set, wait for completion '''
|
|
||||||
|
|
||||||
results = poller.wait(async_seconds, async_poll_interval)
|
|
||||||
|
|
||||||
# mark any hosts that are still listed as started as failed
|
|
||||||
# since these likely got killed by async_wrapper
|
|
||||||
for host in poller.hosts_to_poll:
|
|
||||||
reason = { 'failed' : 1, 'rc' : None, 'msg' : 'timed out' }
|
|
||||||
self.runner_callbacks.on_async_failed(host, reason, poller.runner.vars_cache[host]['ansible_job_id'])
|
|
||||||
results['contacted'][host] = reason
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _trim_unavailable_hosts(self, hostlist=[], keep_failed=False):
|
|
||||||
''' returns a list of hosts that haven't failed and aren't dark '''
|
|
||||||
|
|
||||||
return [ h for h in hostlist if (keep_failed or h not in self.stats.failures) and (h not in self.stats.dark)]
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _run_task_internal(self, task, include_failed=False):
|
|
||||||
''' run a particular module step in a playbook '''
|
|
||||||
|
|
||||||
hosts = self._trim_unavailable_hosts(self.inventory.list_hosts(task.play._play_hosts), keep_failed=include_failed)
|
|
||||||
self.inventory.restrict_to(hosts)
|
|
||||||
|
|
||||||
runner = ansible.runner.Runner(
|
|
||||||
pattern=task.play.hosts,
|
|
||||||
inventory=self.inventory,
|
|
||||||
module_name=task.module_name,
|
|
||||||
module_args=task.module_args,
|
|
||||||
forks=self.forks,
|
|
||||||
remote_pass=self.remote_pass,
|
|
||||||
module_path=self.module_path,
|
|
||||||
timeout=self.timeout,
|
|
||||||
remote_user=task.remote_user,
|
|
||||||
remote_port=task.play.remote_port,
|
|
||||||
module_vars=task.module_vars,
|
|
||||||
play_vars=task.play_vars,
|
|
||||||
play_file_vars=task.play_file_vars,
|
|
||||||
role_vars=task.role_vars,
|
|
||||||
role_params=task.role_params,
|
|
||||||
default_vars=task.default_vars,
|
|
||||||
extra_vars=self.extra_vars,
|
|
||||||
private_key_file=self.private_key_file,
|
|
||||||
setup_cache=self.SETUP_CACHE,
|
|
||||||
vars_cache=self.VARS_CACHE,
|
|
||||||
basedir=task.play.basedir,
|
|
||||||
conditional=task.when,
|
|
||||||
callbacks=self.runner_callbacks,
|
|
||||||
transport=task.transport,
|
|
||||||
is_playbook=True,
|
|
||||||
check=self.check,
|
|
||||||
diff=self.diff,
|
|
||||||
environment=task.environment,
|
|
||||||
complex_args=task.args,
|
|
||||||
accelerate=task.play.accelerate,
|
|
||||||
accelerate_port=task.play.accelerate_port,
|
|
||||||
accelerate_ipv6=task.play.accelerate_ipv6,
|
|
||||||
error_on_undefined_vars=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR,
|
|
||||||
vault_pass = self.vault_password,
|
|
||||||
run_hosts=hosts,
|
|
||||||
no_log=task.no_log,
|
|
||||||
run_once=task.run_once,
|
|
||||||
become=task.become,
|
|
||||||
become_method=task.become_method,
|
|
||||||
become_user=task.become_user,
|
|
||||||
become_pass=task.become_pass,
|
|
||||||
)
|
|
||||||
|
|
||||||
runner.module_vars.update({'play_hosts': hosts})
|
|
||||||
runner.module_vars.update({'ansible_version': self._ansible_version})
|
|
||||||
|
|
||||||
if task.async_seconds == 0:
|
|
||||||
results = runner.run()
|
|
||||||
else:
|
|
||||||
results, poller = runner.run_async(task.async_seconds)
|
|
||||||
self.stats.compute(results)
|
|
||||||
if task.async_poll_interval > 0:
|
|
||||||
# if not polling, playbook requested fire and forget, so don't poll
|
|
||||||
results = self._async_poll(poller, task.async_seconds, task.async_poll_interval)
|
|
||||||
else:
|
|
||||||
for (host, res) in results.get('contacted', {}).iteritems():
|
|
||||||
self.runner_callbacks.on_async_ok(host, res, poller.runner.vars_cache[host]['ansible_job_id'])
|
|
||||||
|
|
||||||
contacted = results.get('contacted',{})
|
|
||||||
dark = results.get('dark', {})
|
|
||||||
|
|
||||||
self.inventory.lift_restriction()
|
|
||||||
|
|
||||||
if len(contacted.keys()) == 0 and len(dark.keys()) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _run_task(self, play, task, is_handler):
|
|
||||||
''' run a single task in the playbook and recursively run any subtasks. '''
|
|
||||||
|
|
||||||
ansible.callbacks.set_task(self.callbacks, task)
|
|
||||||
ansible.callbacks.set_task(self.runner_callbacks, task)
|
|
||||||
|
|
||||||
if task.role_name:
|
|
||||||
name = '%s | %s' % (task.role_name, task.name)
|
|
||||||
else:
|
|
||||||
name = task.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# v1 HACK: we don't have enough information to template many names
|
|
||||||
# at this point. Rather than making this work for all cases in
|
|
||||||
# v1, just make this degrade gracefully. Will fix in v2
|
|
||||||
name = template(play.basedir, name, task.module_vars, lookup_fatal=False, filter_fatal=False)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.callbacks.on_task_start(name, is_handler)
|
|
||||||
if hasattr(self.callbacks, 'skip_task') and self.callbacks.skip_task:
|
|
||||||
ansible.callbacks.set_task(self.callbacks, None)
|
|
||||||
ansible.callbacks.set_task(self.runner_callbacks, None)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# template ignore_errors
|
|
||||||
# TODO: Is this needed here? cond is templated again in
|
|
||||||
# check_conditional after some more manipulations.
|
|
||||||
# TODO: we don't have enough information here to template cond either
|
|
||||||
# (see note on templating name above)
|
|
||||||
cond = template(play.basedir, task.ignore_errors, task.module_vars, expand_lists=False)
|
|
||||||
task.ignore_errors = utils.check_conditional(cond, play.basedir, task.module_vars, fail_on_undefined=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR)
|
|
||||||
|
|
||||||
# load up an appropriate ansible runner to run the task in parallel
|
|
||||||
include_failed = is_handler and play.force_handlers
|
|
||||||
results = self._run_task_internal(task, include_failed=include_failed)
|
|
||||||
|
|
||||||
# if no hosts are matched, carry on
|
|
||||||
hosts_remaining = True
|
|
||||||
if results is None:
|
|
||||||
hosts_remaining = False
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
contacted = results.get('contacted', {})
|
|
||||||
self.stats.compute(results, ignore_errors=task.ignore_errors)
|
|
||||||
|
|
||||||
def _register_play_vars(host, result):
|
|
||||||
# when 'register' is used, persist the result in the vars cache
|
|
||||||
# rather than the setup cache - vars should be transient between
|
|
||||||
# playbook executions
|
|
||||||
if 'stdout' in result and 'stdout_lines' not in result:
|
|
||||||
result['stdout_lines'] = result['stdout'].splitlines()
|
|
||||||
utils.update_hash(self.VARS_CACHE, host, {task.register: result})
|
|
||||||
|
|
||||||
def _save_play_facts(host, facts):
|
|
||||||
# saves play facts in SETUP_CACHE, unless the module executed was
|
|
||||||
# set_fact, in which case we add them to the VARS_CACHE
|
|
||||||
if task.module_name in ('set_fact', 'include_vars'):
|
|
||||||
utils.update_hash(self.VARS_CACHE, host, facts)
|
|
||||||
else:
|
|
||||||
utils.update_hash(self.SETUP_CACHE, host, facts)
|
|
||||||
|
|
||||||
# add facts to the global setup cache
|
|
||||||
for host, result in contacted.iteritems():
|
|
||||||
if 'results' in result:
|
|
||||||
# task ran with_ lookup plugin, so facts are encapsulated in
|
|
||||||
# multiple list items in the results key
|
|
||||||
for res in result['results']:
|
|
||||||
if type(res) == dict:
|
|
||||||
facts = res.get('ansible_facts', {})
|
|
||||||
_save_play_facts(host, facts)
|
|
||||||
else:
|
|
||||||
# when facts are returned, persist them in the setup cache
|
|
||||||
facts = result.get('ansible_facts', {})
|
|
||||||
_save_play_facts(host, facts)
|
|
||||||
|
|
||||||
# if requested, save the result into the registered variable name
|
|
||||||
if task.register:
|
|
||||||
_register_play_vars(host, result)
|
|
||||||
|
|
||||||
# also have to register some failed, but ignored, tasks
|
|
||||||
if task.ignore_errors and task.register:
|
|
||||||
failed = results.get('failed', {})
|
|
||||||
for host, result in failed.iteritems():
|
|
||||||
_register_play_vars(host, result)
|
|
||||||
|
|
||||||
# flag which notify handlers need to be run
|
|
||||||
if len(task.notify) > 0:
|
|
||||||
for host, results in results.get('contacted',{}).iteritems():
|
|
||||||
if results.get('changed', False):
|
|
||||||
for handler_name in task.notify:
|
|
||||||
self._flag_handler(play, template(play.basedir, handler_name, task.module_vars), host)
|
|
||||||
|
|
||||||
ansible.callbacks.set_task(self.callbacks, None)
|
|
||||||
ansible.callbacks.set_task(self.runner_callbacks, None)
|
|
||||||
return hosts_remaining
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _flag_handler(self, play, handler_name, host):
|
|
||||||
'''
|
|
||||||
if a task has any notify elements, flag handlers for run
|
|
||||||
at end of execution cycle for hosts that have indicated
|
|
||||||
changes have been made
|
|
||||||
'''
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for x in play.handlers():
|
|
||||||
if handler_name == template(play.basedir, x.name, x.module_vars):
|
|
||||||
found = True
|
|
||||||
self.callbacks.on_notify(host, x.name)
|
|
||||||
x.notified_by.append(host)
|
|
||||||
if not found:
|
|
||||||
raise errors.AnsibleError("change handler (%s) is not defined" % handler_name)
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
def _do_setup_step(self, play):
|
|
||||||
''' get facts from the remote system '''
|
|
||||||
|
|
||||||
host_list = self._trim_unavailable_hosts(play._play_hosts)
|
|
||||||
|
|
||||||
if play.gather_facts is None and C.DEFAULT_GATHERING == 'smart':
|
|
||||||
host_list = [h for h in host_list if h not in self.SETUP_CACHE or 'module_setup' not in self.SETUP_CACHE[h]]
|
|
||||||
if len(host_list) == 0:
|
|
||||||
return {}
|
|
||||||
elif play.gather_facts is False or (play.gather_facts is None and C.DEFAULT_GATHERING == 'explicit'):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
self.callbacks.on_setup()
|
|
||||||
self.inventory.restrict_to(host_list)
|
|
||||||
|
|
||||||
ansible.callbacks.set_task(self.callbacks, None)
|
|
||||||
ansible.callbacks.set_task(self.runner_callbacks, None)
|
|
||||||
|
|
||||||
# push any variables down to the system
|
|
||||||
setup_results = ansible.runner.Runner(
|
|
||||||
basedir=self.basedir,
|
|
||||||
pattern=play.hosts,
|
|
||||||
module_name='setup',
|
|
||||||
module_args={},
|
|
||||||
inventory=self.inventory,
|
|
||||||
forks=self.forks,
|
|
||||||
module_path=self.module_path,
|
|
||||||
timeout=self.timeout,
|
|
||||||
remote_user=play.remote_user,
|
|
||||||
remote_pass=self.remote_pass,
|
|
||||||
remote_port=play.remote_port,
|
|
||||||
private_key_file=self.private_key_file,
|
|
||||||
setup_cache=self.SETUP_CACHE,
|
|
||||||
vars_cache=self.VARS_CACHE,
|
|
||||||
callbacks=self.runner_callbacks,
|
|
||||||
become=play.become,
|
|
||||||
become_method=play.become_method,
|
|
||||||
become_user=play.become_user,
|
|
||||||
become_pass=self.become_pass,
|
|
||||||
vault_pass=self.vault_password,
|
|
||||||
transport=play.transport,
|
|
||||||
is_playbook=True,
|
|
||||||
module_vars=play.vars,
|
|
||||||
play_vars=play.vars,
|
|
||||||
play_file_vars=play.vars_file_vars,
|
|
||||||
role_vars=play.role_vars,
|
|
||||||
default_vars=play.default_vars,
|
|
||||||
check=self.check,
|
|
||||||
diff=self.diff,
|
|
||||||
accelerate=play.accelerate,
|
|
||||||
accelerate_port=play.accelerate_port,
|
|
||||||
).run()
|
|
||||||
self.stats.compute(setup_results, setup=True)
|
|
||||||
|
|
||||||
self.inventory.lift_restriction()
|
|
||||||
|
|
||||||
# now for each result, load into the setup cache so we can
|
|
||||||
# let runner template out future commands
|
|
||||||
setup_ok = setup_results.get('contacted', {})
|
|
||||||
for (host, result) in setup_ok.iteritems():
|
|
||||||
utils.update_hash(self.SETUP_CACHE, host, {'module_setup': True})
|
|
||||||
utils.update_hash(self.SETUP_CACHE, host, result.get('ansible_facts', {}))
|
|
||||||
return setup_results
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
|
|
||||||
|
|
||||||
def generate_retry_inventory(self, replay_hosts):
|
|
||||||
'''
|
|
||||||
called by /usr/bin/ansible when a playbook run fails. It generates an inventory
|
|
||||||
that allows re-running on ONLY the failed hosts. This may duplicate some
|
|
||||||
variable information in group_vars/host_vars but that is ok, and expected.
|
|
||||||
'''
|
|
||||||
|
|
||||||
buf = StringIO.StringIO()
|
|
||||||
for x in replay_hosts:
|
|
||||||
buf.write("%s\n" % x)
|
|
||||||
basedir = C.shell_expand_path(C.RETRY_FILES_SAVE_PATH)
|
|
||||||
filename = "%s.retry" % os.path.basename(self.filename)
|
|
||||||
filename = filename.replace(".yml","")
|
|
||||||
filename = os.path.join(basedir, filename)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not os.path.exists(basedir):
|
|
||||||
os.makedirs(basedir)
|
|
||||||
|
|
||||||
fd = open(filename, 'w')
|
|
||||||
fd.write(buf.getvalue())
|
|
||||||
fd.close()
|
|
||||||
except:
|
|
||||||
ansible.callbacks.display(
|
|
||||||
"\nERROR: could not create retry file. Check the value of \n"
|
|
||||||
+ "the configuration variable 'retry_files_save_path' or set \n"
|
|
||||||
+ "'retry_files_enabled' to False to avoid this message.\n",
|
|
||||||
color='red'
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return filename
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
def tasks_to_run_in_play(self, play):
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
|
|
||||||
for task in play.tasks():
|
|
||||||
# only run the task if the requested tags match or has 'always' tag
|
|
||||||
u = set(['untagged'])
|
|
||||||
task_set = set(task.tags)
|
|
||||||
|
|
||||||
if 'always' in task.tags:
|
|
||||||
should_run = True
|
|
||||||
else:
|
|
||||||
if 'all' in self.only_tags:
|
|
||||||
should_run = True
|
|
||||||
else:
|
|
||||||
should_run = False
|
|
||||||
if 'tagged' in self.only_tags:
|
|
||||||
if task_set != u:
|
|
||||||
should_run = True
|
|
||||||
elif 'untagged' in self.only_tags:
|
|
||||||
if task_set == u:
|
|
||||||
should_run = True
|
|
||||||
else:
|
|
||||||
if task_set.intersection(self.only_tags):
|
|
||||||
should_run = True
|
|
||||||
|
|
||||||
# Check for tags that we need to skip
|
|
||||||
if 'all' in self.skip_tags:
|
|
||||||
should_run = False
|
|
||||||
else:
|
|
||||||
if 'tagged' in self.skip_tags:
|
|
||||||
if task_set != u:
|
|
||||||
should_run = False
|
|
||||||
elif 'untagged' in self.skip_tags:
|
|
||||||
if task_set == u:
|
|
||||||
should_run = False
|
|
||||||
else:
|
|
||||||
if should_run:
|
|
||||||
if task_set.intersection(self.skip_tags):
|
|
||||||
should_run = False
|
|
||||||
|
|
||||||
if should_run:
|
|
||||||
tasks.append(task)
|
|
||||||
|
|
||||||
return tasks
|
|
||||||
|
|
||||||
# *****************************************************
|
|
||||||
def _run_play(self, play):
|
|
||||||
''' run a list of tasks for a given pattern, in order '''
|
|
||||||
|
|
||||||
self.callbacks.on_play_start(play.name)
|
|
||||||
# Get the hosts for this play
|
|
||||||
play._play_hosts = self.inventory.list_hosts(play.hosts)
|
|
||||||
# if no hosts matches this play, drop out
|
|
||||||
if not play._play_hosts:
|
|
||||||
self.callbacks.on_no_hosts_matched()
|
|
||||||
return True
|
|
||||||
|
|
||||||
# get facts from system
|
|
||||||
self._do_setup_step(play)
|
|
||||||
|
|
||||||
# now with that data, handle contentional variable file imports!
|
|
||||||
all_hosts = self._trim_unavailable_hosts(play._play_hosts)
|
|
||||||
play.update_vars_files(all_hosts, vault_password=self.vault_password)
|
|
||||||
hosts_count = len(all_hosts)
|
|
||||||
|
|
||||||
if play.serial.endswith("%"):
|
|
||||||
|
|
||||||
# This is a percentage, so calculate it based on the
|
|
||||||
# number of hosts
|
|
||||||
serial_pct = int(play.serial.replace("%",""))
|
|
||||||
serial = int((serial_pct/100.0) * len(all_hosts))
|
|
||||||
|
|
||||||
# Ensure that no matter how small the percentage, serial
|
|
||||||
# can never fall below 1, so that things actually happen
|
|
||||||
serial = max(serial, 1)
|
|
||||||
else:
|
|
||||||
serial = int(play.serial)
|
|
||||||
|
|
||||||
serialized_batch = []
|
|
||||||
if serial <= 0:
|
|
||||||
serialized_batch = [all_hosts]
|
|
||||||
else:
|
|
||||||
# do N forks all the way through before moving to next
|
|
||||||
while len(all_hosts) > 0:
|
|
||||||
play_hosts = []
|
|
||||||
for x in range(serial):
|
|
||||||
if len(all_hosts) > 0:
|
|
||||||
play_hosts.append(all_hosts.pop(0))
|
|
||||||
serialized_batch.append(play_hosts)
|
|
||||||
|
|
||||||
task_errors = False
|
|
||||||
for on_hosts in serialized_batch:
|
|
||||||
|
|
||||||
# restrict the play to just the hosts we have in our on_hosts block that are
|
|
||||||
# available.
|
|
||||||
play._play_hosts = self._trim_unavailable_hosts(on_hosts)
|
|
||||||
self.inventory.also_restrict_to(on_hosts)
|
|
||||||
|
|
||||||
for task in self.tasks_to_run_in_play(play):
|
|
||||||
|
|
||||||
if task.meta is not None:
|
|
||||||
# meta tasks can force handlers to run mid-play
|
|
||||||
if task.meta == 'flush_handlers':
|
|
||||||
self.run_handlers(play)
|
|
||||||
|
|
||||||
# skip calling the handler till the play is finished
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not self._run_task(play, task, False):
|
|
||||||
# whether no hosts matched is fatal or not depends if it was on the initial step.
|
|
||||||
# if we got exactly no hosts on the first step (setup!) then the host group
|
|
||||||
# just didn't match anything and that's ok
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get a new list of what hosts are left as available, the ones that
|
|
||||||
# did not go fail/dark during the task
|
|
||||||
host_list = self._trim_unavailable_hosts(play._play_hosts)
|
|
||||||
|
|
||||||
# Set max_fail_pct to 0, So if any hosts fails, bail out
|
|
||||||
if task.any_errors_fatal and len(host_list) < hosts_count:
|
|
||||||
play.max_fail_pct = 0
|
|
||||||
|
|
||||||
# If threshold for max nodes failed is exceeded, bail out.
|
|
||||||
if play.serial > 0:
|
|
||||||
# if serial is set, we need to shorten the size of host_count
|
|
||||||
play_count = len(play._play_hosts)
|
|
||||||
if (play_count - len(host_list)) > int((play.max_fail_pct)/100.0 * play_count):
|
|
||||||
host_list = None
|
|
||||||
else:
|
|
||||||
if (hosts_count - len(host_list)) > int((play.max_fail_pct)/100.0 * hosts_count):
|
|
||||||
host_list = None
|
|
||||||
|
|
||||||
# if no hosts remain, drop out
|
|
||||||
if not host_list:
|
|
||||||
if play.force_handlers:
|
|
||||||
task_errors = True
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.callbacks.on_no_hosts_remaining()
|
|
||||||
return False
|
|
||||||
|
|
||||||
# lift restrictions after each play finishes
|
|
||||||
self.inventory.lift_also_restriction()
|
|
||||||
|
|
||||||
if task_errors and not play.force_handlers:
|
|
||||||
# if there were failed tasks and handler execution
|
|
||||||
# is not forced, quit the play with an error
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
# no errors, go ahead and execute all handlers
|
|
||||||
if not self.run_handlers(play):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def run_handlers(self, play):
|
|
||||||
on_hosts = play._play_hosts
|
|
||||||
hosts_count = len(on_hosts)
|
|
||||||
for task in play.tasks():
|
|
||||||
if task.meta is not None:
|
|
||||||
|
|
||||||
fired_names = {}
|
|
||||||
for handler in play.handlers():
|
|
||||||
if len(handler.notified_by) > 0:
|
|
||||||
self.inventory.restrict_to(handler.notified_by)
|
|
||||||
|
|
||||||
# Resolve the variables first
|
|
||||||
handler_name = template(play.basedir, handler.name, handler.module_vars)
|
|
||||||
if handler_name not in fired_names:
|
|
||||||
self._run_task(play, handler, True)
|
|
||||||
# prevent duplicate handler includes from running more than once
|
|
||||||
fired_names[handler_name] = 1
|
|
||||||
|
|
||||||
host_list = self._trim_unavailable_hosts(play._play_hosts)
|
|
||||||
if handler.any_errors_fatal and len(host_list) < hosts_count:
|
|
||||||
play.max_fail_pct = 0
|
|
||||||
if (hosts_count - len(host_list)) > int((play.max_fail_pct)/100.0 * hosts_count):
|
|
||||||
host_list = None
|
|
||||||
if not host_list and not play.force_handlers:
|
|
||||||
self.callbacks.on_no_hosts_remaining()
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.inventory.lift_restriction()
|
|
||||||
new_list = handler.notified_by[:]
|
|
||||||
for host in handler.notified_by:
|
|
||||||
if host in on_hosts:
|
|
||||||
while host in new_list:
|
|
||||||
new_list.remove(host)
|
|
||||||
handler.notified_by = new_list
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
return True
|
|
@ -1,949 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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.utils.template import template
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.playbook.task import Task
|
|
||||||
from ansible.module_utils.splitter import split_args, unquote
|
|
||||||
import ansible.constants as C
|
|
||||||
import pipes
|
|
||||||
import shlex
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class Play(object):
|
|
||||||
|
|
||||||
_pb_common = [
|
|
||||||
'accelerate', 'accelerate_ipv6', 'accelerate_port', 'any_errors_fatal', 'become',
|
|
||||||
'become_method', 'become_user', 'environment', 'force_handlers', 'gather_facts',
|
|
||||||
'handlers', 'hosts', 'name', 'no_log', 'remote_user', 'roles', 'serial', 'su',
|
|
||||||
'su_user', 'sudo', 'sudo_user', 'tags', 'vars', 'vars_files', 'vars_prompt',
|
|
||||||
'vault_password',
|
|
||||||
]
|
|
||||||
|
|
||||||
__slots__ = _pb_common + [
|
|
||||||
'_ds', '_handlers', '_play_hosts', '_tasks', 'any_errors_fatal', 'basedir',
|
|
||||||
'default_vars', 'included_roles', 'max_fail_pct', 'playbook', 'remote_port',
|
|
||||||
'role_vars', 'transport', 'vars_file_vars',
|
|
||||||
]
|
|
||||||
|
|
||||||
# to catch typos and so forth -- these are userland names
|
|
||||||
# and don't line up 1:1 with how they are stored
|
|
||||||
VALID_KEYS = frozenset(_pb_common + [
|
|
||||||
'connection', 'include', 'max_fail_percentage', 'port', 'post_tasks',
|
|
||||||
'pre_tasks', 'role_names', 'tasks', 'user',
|
|
||||||
])
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def __init__(self, playbook, ds, basedir, vault_password=None):
|
|
||||||
''' constructor loads from a play datastructure '''
|
|
||||||
|
|
||||||
for x in ds.keys():
|
|
||||||
if not x in Play.VALID_KEYS:
|
|
||||||
raise errors.AnsibleError("%s is not a legal parameter of an Ansible Play" % x)
|
|
||||||
|
|
||||||
# allow all playbook keys to be set by --extra-vars
|
|
||||||
self.vars = ds.get('vars', {})
|
|
||||||
self.vars_prompt = ds.get('vars_prompt', {})
|
|
||||||
self.playbook = playbook
|
|
||||||
self.vars = self._get_vars()
|
|
||||||
self.vars_file_vars = dict() # these are vars read in from vars_files:
|
|
||||||
self.role_vars = dict() # these are vars read in from vars/main.yml files in roles
|
|
||||||
self.basedir = basedir
|
|
||||||
self.roles = ds.get('roles', None)
|
|
||||||
self.tags = ds.get('tags', None)
|
|
||||||
self.vault_password = vault_password
|
|
||||||
self.environment = ds.get('environment', {})
|
|
||||||
|
|
||||||
if self.tags is None:
|
|
||||||
self.tags = []
|
|
||||||
elif type(self.tags) in [ str, unicode ]:
|
|
||||||
self.tags = self.tags.split(",")
|
|
||||||
elif type(self.tags) != list:
|
|
||||||
self.tags = []
|
|
||||||
|
|
||||||
# make sure we have some special internal variables set, which
|
|
||||||
# we use later when loading tasks and handlers
|
|
||||||
load_vars = dict()
|
|
||||||
load_vars['playbook_dir'] = os.path.abspath(self.basedir)
|
|
||||||
if self.playbook.inventory.basedir() is not None:
|
|
||||||
load_vars['inventory_dir'] = self.playbook.inventory.basedir()
|
|
||||||
if self.playbook.inventory.src() is not None:
|
|
||||||
load_vars['inventory_file'] = self.playbook.inventory.src()
|
|
||||||
|
|
||||||
# We first load the vars files from the datastructure
|
|
||||||
# so we have the default variables to pass into the roles
|
|
||||||
self.vars_files = ds.get('vars_files', [])
|
|
||||||
if not isinstance(self.vars_files, list):
|
|
||||||
raise errors.AnsibleError('vars_files must be a list')
|
|
||||||
processed_vars_files = self._update_vars_files_for_host(None)
|
|
||||||
|
|
||||||
# now we load the roles into the datastructure
|
|
||||||
self.included_roles = []
|
|
||||||
ds = self._load_roles(self.roles, ds)
|
|
||||||
|
|
||||||
# and finally re-process the vars files as they may have been updated
|
|
||||||
# by the included roles, but exclude any which have been processed
|
|
||||||
self.vars_files = utils.list_difference(ds.get('vars_files', []), processed_vars_files)
|
|
||||||
if not isinstance(self.vars_files, list):
|
|
||||||
raise errors.AnsibleError('vars_files must be a list')
|
|
||||||
|
|
||||||
self._update_vars_files_for_host(None)
|
|
||||||
|
|
||||||
# template everything to be efficient, but do not pre-mature template
|
|
||||||
# tasks/handlers as they may have inventory scope overrides. We also
|
|
||||||
# create a set of temporary variables for templating, so we don't
|
|
||||||
# trample on the existing vars structures
|
|
||||||
_tasks = ds.pop('tasks', [])
|
|
||||||
_handlers = ds.pop('handlers', [])
|
|
||||||
|
|
||||||
temp_vars = utils.combine_vars(self.vars, self.vars_file_vars)
|
|
||||||
temp_vars = utils.combine_vars(temp_vars, self.playbook.extra_vars)
|
|
||||||
|
|
||||||
try:
|
|
||||||
ds = template(basedir, ds, temp_vars)
|
|
||||||
except errors.AnsibleError, e:
|
|
||||||
utils.warning("non fatal error while trying to template play variables: %s" % (str(e)))
|
|
||||||
|
|
||||||
ds['tasks'] = _tasks
|
|
||||||
ds['handlers'] = _handlers
|
|
||||||
|
|
||||||
self._ds = ds
|
|
||||||
|
|
||||||
hosts = ds.get('hosts')
|
|
||||||
if hosts is None:
|
|
||||||
raise errors.AnsibleError('hosts declaration is required')
|
|
||||||
elif isinstance(hosts, list):
|
|
||||||
try:
|
|
||||||
hosts = ';'.join(hosts)
|
|
||||||
except TypeError,e:
|
|
||||||
raise errors.AnsibleError('improper host declaration: %s' % str(e))
|
|
||||||
|
|
||||||
self.serial = str(ds.get('serial', 0))
|
|
||||||
self.hosts = hosts
|
|
||||||
self.name = ds.get('name', self.hosts)
|
|
||||||
self._tasks = ds.get('tasks', [])
|
|
||||||
self._handlers = ds.get('handlers', [])
|
|
||||||
self.remote_user = ds.get('remote_user', ds.get('user', self.playbook.remote_user))
|
|
||||||
self.remote_port = ds.get('port', self.playbook.remote_port)
|
|
||||||
self.transport = ds.get('connection', self.playbook.transport)
|
|
||||||
self.remote_port = self.remote_port
|
|
||||||
self.any_errors_fatal = utils.boolean(ds.get('any_errors_fatal', 'false'))
|
|
||||||
self.accelerate = utils.boolean(ds.get('accelerate', 'false'))
|
|
||||||
self.accelerate_port = ds.get('accelerate_port', None)
|
|
||||||
self.accelerate_ipv6 = ds.get('accelerate_ipv6', False)
|
|
||||||
self.max_fail_pct = int(ds.get('max_fail_percentage', 100))
|
|
||||||
self.no_log = utils.boolean(ds.get('no_log', 'false'))
|
|
||||||
self.force_handlers = utils.boolean(ds.get('force_handlers', self.playbook.force_handlers))
|
|
||||||
|
|
||||||
# Fail out if user specifies conflicting privilege escalations
|
|
||||||
if (ds.get('become') or ds.get('become_user')) and (ds.get('sudo') or ds.get('sudo_user')):
|
|
||||||
raise errors.AnsibleError('sudo params ("become", "become_user") and su params ("sudo", "sudo_user") cannot be used together')
|
|
||||||
if (ds.get('become') or ds.get('become_user')) and (ds.get('su') or ds.get('su_user')):
|
|
||||||
raise errors.AnsibleError('sudo params ("become", "become_user") and su params ("su", "su_user") cannot be used together')
|
|
||||||
if (ds.get('sudo') or ds.get('sudo_user')) and (ds.get('su') or ds.get('su_user')):
|
|
||||||
raise errors.AnsibleError('sudo params ("sudo", "sudo_user") and su params ("su", "su_user") cannot be used together')
|
|
||||||
|
|
||||||
# become settings are inherited and updated normally
|
|
||||||
self.become = ds.get('become', self.playbook.become)
|
|
||||||
self.become_method = ds.get('become_method', self.playbook.become_method)
|
|
||||||
self.become_user = ds.get('become_user', self.playbook.become_user)
|
|
||||||
|
|
||||||
# Make sure current play settings are reflected in become fields
|
|
||||||
if 'sudo' in ds:
|
|
||||||
self.become=ds['sudo']
|
|
||||||
self.become_method='sudo'
|
|
||||||
if 'sudo_user' in ds:
|
|
||||||
self.become_user=ds['sudo_user']
|
|
||||||
elif 'su' in ds:
|
|
||||||
self.become=True
|
|
||||||
self.become=ds['su']
|
|
||||||
self.become_method='su'
|
|
||||||
if 'su_user' in ds:
|
|
||||||
self.become_user=ds['su_user']
|
|
||||||
|
|
||||||
# gather_facts is not a simple boolean, as None means that a 'smart'
|
|
||||||
# fact gathering mode will be used, so we need to be careful here as
|
|
||||||
# calling utils.boolean(None) returns False
|
|
||||||
self.gather_facts = ds.get('gather_facts', None)
|
|
||||||
if self.gather_facts is not None:
|
|
||||||
self.gather_facts = utils.boolean(self.gather_facts)
|
|
||||||
|
|
||||||
load_vars['role_names'] = ds.get('role_names', [])
|
|
||||||
|
|
||||||
self._tasks = self._load_tasks(self._ds.get('tasks', []), load_vars)
|
|
||||||
self._handlers = self._load_tasks(self._ds.get('handlers', []), load_vars)
|
|
||||||
|
|
||||||
# apply any missing tags to role tasks
|
|
||||||
self._late_merge_role_tags()
|
|
||||||
|
|
||||||
# place holder for the discovered hosts to be used in this play
|
|
||||||
self._play_hosts = None
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def _get_role_path(self, role):
|
|
||||||
"""
|
|
||||||
Returns the path on disk to the directory containing
|
|
||||||
the role directories like tasks, templates, etc. Also
|
|
||||||
returns any variables that were included with the role
|
|
||||||
"""
|
|
||||||
orig_path = template(self.basedir,role,self.vars)
|
|
||||||
|
|
||||||
role_vars = {}
|
|
||||||
if type(orig_path) == dict:
|
|
||||||
# what, not a path?
|
|
||||||
role_name = orig_path.get('role', None)
|
|
||||||
if role_name is None:
|
|
||||||
raise errors.AnsibleError("expected a role name in dictionary: %s" % orig_path)
|
|
||||||
role_vars = orig_path
|
|
||||||
else:
|
|
||||||
role_name = utils.role_spec_parse(orig_path)["name"]
|
|
||||||
|
|
||||||
role_path = None
|
|
||||||
|
|
||||||
possible_paths = [
|
|
||||||
utils.path_dwim(self.basedir, os.path.join('roles', role_name)),
|
|
||||||
utils.path_dwim(self.basedir, role_name)
|
|
||||||
]
|
|
||||||
|
|
||||||
if C.DEFAULT_ROLES_PATH:
|
|
||||||
search_locations = C.DEFAULT_ROLES_PATH.split(os.pathsep)
|
|
||||||
for loc in search_locations:
|
|
||||||
loc = os.path.expanduser(loc)
|
|
||||||
possible_paths.append(utils.path_dwim(loc, role_name))
|
|
||||||
|
|
||||||
for path_option in possible_paths:
|
|
||||||
if os.path.isdir(path_option):
|
|
||||||
role_path = path_option
|
|
||||||
break
|
|
||||||
|
|
||||||
if role_path is None:
|
|
||||||
raise errors.AnsibleError("cannot find role in %s" % " or ".join(possible_paths))
|
|
||||||
|
|
||||||
return (role_path, role_vars)
|
|
||||||
|
|
||||||
def _build_role_dependencies(self, roles, dep_stack, passed_vars={}, level=0):
|
|
||||||
# this number is arbitrary, but it seems sane
|
|
||||||
if level > 20:
|
|
||||||
raise errors.AnsibleError("too many levels of recursion while resolving role dependencies")
|
|
||||||
for role in roles:
|
|
||||||
role_path,role_vars = self._get_role_path(role)
|
|
||||||
|
|
||||||
# save just the role params for this role, which exclude the special
|
|
||||||
# keywords 'role', 'tags', and 'when'.
|
|
||||||
role_params = role_vars.copy()
|
|
||||||
for item in ('role', 'tags', 'when'):
|
|
||||||
if item in role_params:
|
|
||||||
del role_params[item]
|
|
||||||
|
|
||||||
role_vars = utils.combine_vars(passed_vars, role_vars)
|
|
||||||
|
|
||||||
vars = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(role_path, 'vars')))
|
|
||||||
vars_data = {}
|
|
||||||
if os.path.isfile(vars):
|
|
||||||
vars_data = utils.parse_yaml_from_file(vars, vault_password=self.vault_password)
|
|
||||||
if vars_data:
|
|
||||||
if not isinstance(vars_data, dict):
|
|
||||||
raise errors.AnsibleError("vars from '%s' are not a dict" % vars)
|
|
||||||
role_vars = utils.combine_vars(vars_data, role_vars)
|
|
||||||
|
|
||||||
defaults = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(role_path, 'defaults')))
|
|
||||||
defaults_data = {}
|
|
||||||
if os.path.isfile(defaults):
|
|
||||||
defaults_data = utils.parse_yaml_from_file(defaults, vault_password=self.vault_password)
|
|
||||||
|
|
||||||
# the meta directory contains the yaml that should
|
|
||||||
# hold the list of dependencies (if any)
|
|
||||||
meta = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(role_path, 'meta')))
|
|
||||||
if os.path.isfile(meta):
|
|
||||||
data = utils.parse_yaml_from_file(meta, vault_password=self.vault_password)
|
|
||||||
if data:
|
|
||||||
dependencies = data.get('dependencies',[])
|
|
||||||
if dependencies is None:
|
|
||||||
dependencies = []
|
|
||||||
for dep in dependencies:
|
|
||||||
allow_dupes = False
|
|
||||||
(dep_path,dep_vars) = self._get_role_path(dep)
|
|
||||||
|
|
||||||
# save the dep params, just as we did above
|
|
||||||
dep_params = dep_vars.copy()
|
|
||||||
for item in ('role', 'tags', 'when'):
|
|
||||||
if item in dep_params:
|
|
||||||
del dep_params[item]
|
|
||||||
|
|
||||||
meta = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(dep_path, 'meta')))
|
|
||||||
if os.path.isfile(meta):
|
|
||||||
meta_data = utils.parse_yaml_from_file(meta, vault_password=self.vault_password)
|
|
||||||
if meta_data:
|
|
||||||
allow_dupes = utils.boolean(meta_data.get('allow_duplicates',''))
|
|
||||||
|
|
||||||
# if any tags were specified as role/dep variables, merge
|
|
||||||
# them into the current dep_vars so they're passed on to any
|
|
||||||
# further dependencies too, and so we only have one place
|
|
||||||
# (dep_vars) to look for tags going forward
|
|
||||||
def __merge_tags(var_obj):
|
|
||||||
old_tags = dep_vars.get('tags', [])
|
|
||||||
if isinstance(old_tags, basestring):
|
|
||||||
old_tags = [old_tags, ]
|
|
||||||
if isinstance(var_obj, dict):
|
|
||||||
new_tags = var_obj.get('tags', [])
|
|
||||||
if isinstance(new_tags, basestring):
|
|
||||||
new_tags = [new_tags, ]
|
|
||||||
else:
|
|
||||||
new_tags = []
|
|
||||||
return list(set(old_tags).union(set(new_tags)))
|
|
||||||
|
|
||||||
dep_vars['tags'] = __merge_tags(role_vars)
|
|
||||||
dep_vars['tags'] = __merge_tags(passed_vars)
|
|
||||||
|
|
||||||
# if tags are set from this role, merge them
|
|
||||||
# into the tags list for the dependent role
|
|
||||||
if "tags" in passed_vars:
|
|
||||||
for included_role_dep in dep_stack:
|
|
||||||
included_dep_name = included_role_dep[0]
|
|
||||||
included_dep_vars = included_role_dep[2]
|
|
||||||
if included_dep_name == dep:
|
|
||||||
if "tags" in included_dep_vars:
|
|
||||||
included_dep_vars["tags"] = list(set(included_dep_vars["tags"]).union(set(passed_vars["tags"])))
|
|
||||||
else:
|
|
||||||
included_dep_vars["tags"] = passed_vars["tags"][:]
|
|
||||||
|
|
||||||
dep_vars = utils.combine_vars(passed_vars, dep_vars)
|
|
||||||
dep_vars = utils.combine_vars(role_vars, dep_vars)
|
|
||||||
|
|
||||||
vars = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(dep_path, 'vars')))
|
|
||||||
vars_data = {}
|
|
||||||
if os.path.isfile(vars):
|
|
||||||
vars_data = utils.parse_yaml_from_file(vars, vault_password=self.vault_password)
|
|
||||||
if vars_data:
|
|
||||||
dep_vars = utils.combine_vars(dep_vars, vars_data)
|
|
||||||
pass
|
|
||||||
|
|
||||||
defaults = self._resolve_main(utils.path_dwim(self.basedir, os.path.join(dep_path, 'defaults')))
|
|
||||||
dep_defaults_data = {}
|
|
||||||
if os.path.isfile(defaults):
|
|
||||||
dep_defaults_data = utils.parse_yaml_from_file(defaults, vault_password=self.vault_password)
|
|
||||||
if 'role' in dep_vars:
|
|
||||||
del dep_vars['role']
|
|
||||||
|
|
||||||
if not allow_dupes:
|
|
||||||
if dep in self.included_roles:
|
|
||||||
# skip back to the top, since we don't want to
|
|
||||||
# do anything else with this role
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.included_roles.append(dep)
|
|
||||||
|
|
||||||
def _merge_conditional(cur_conditionals, new_conditionals):
|
|
||||||
if isinstance(new_conditionals, (basestring, bool)):
|
|
||||||
cur_conditionals.append(new_conditionals)
|
|
||||||
elif isinstance(new_conditionals, list):
|
|
||||||
cur_conditionals.extend(new_conditionals)
|
|
||||||
|
|
||||||
# pass along conditionals from roles to dep roles
|
|
||||||
passed_when = passed_vars.get('when')
|
|
||||||
role_when = role_vars.get('when')
|
|
||||||
dep_when = dep_vars.get('when')
|
|
||||||
|
|
||||||
tmpcond = []
|
|
||||||
_merge_conditional(tmpcond, passed_when)
|
|
||||||
_merge_conditional(tmpcond, role_when)
|
|
||||||
_merge_conditional(tmpcond, dep_when)
|
|
||||||
|
|
||||||
if len(tmpcond) > 0:
|
|
||||||
dep_vars['when'] = tmpcond
|
|
||||||
|
|
||||||
self._build_role_dependencies([dep], dep_stack, passed_vars=dep_vars, level=level+1)
|
|
||||||
dep_stack.append([dep, dep_path, dep_vars, dep_params, dep_defaults_data])
|
|
||||||
|
|
||||||
# only add the current role when we're at the top level,
|
|
||||||
# otherwise we'll end up in a recursive loop
|
|
||||||
if level == 0:
|
|
||||||
self.included_roles.append(role)
|
|
||||||
dep_stack.append([role, role_path, role_vars, role_params, defaults_data])
|
|
||||||
return dep_stack
|
|
||||||
|
|
||||||
def _load_role_vars_files(self, vars_files):
|
|
||||||
# process variables stored in vars/main.yml files
|
|
||||||
role_vars = {}
|
|
||||||
for filename in vars_files:
|
|
||||||
if os.path.exists(filename):
|
|
||||||
new_vars = utils.parse_yaml_from_file(filename, vault_password=self.vault_password)
|
|
||||||
if new_vars:
|
|
||||||
if type(new_vars) != dict:
|
|
||||||
raise errors.AnsibleError("%s must be stored as dictionary/hash: %s" % (filename, type(new_vars)))
|
|
||||||
role_vars = utils.combine_vars(role_vars, new_vars)
|
|
||||||
|
|
||||||
return role_vars
|
|
||||||
|
|
||||||
def _load_role_defaults(self, defaults_files):
|
|
||||||
# process default variables
|
|
||||||
default_vars = {}
|
|
||||||
for filename in defaults_files:
|
|
||||||
if os.path.exists(filename):
|
|
||||||
new_default_vars = utils.parse_yaml_from_file(filename, vault_password=self.vault_password)
|
|
||||||
if new_default_vars:
|
|
||||||
if type(new_default_vars) != dict:
|
|
||||||
raise errors.AnsibleError("%s must be stored as dictionary/hash: %s" % (filename, type(new_default_vars)))
|
|
||||||
default_vars = utils.combine_vars(default_vars, new_default_vars)
|
|
||||||
|
|
||||||
return default_vars
|
|
||||||
|
|
||||||
def _load_roles(self, roles, ds):
|
|
||||||
# a role is a name that auto-includes the following if they exist
|
|
||||||
# <rolename>/tasks/main.yml
|
|
||||||
# <rolename>/handlers/main.yml
|
|
||||||
# <rolename>/vars/main.yml
|
|
||||||
# <rolename>/library
|
|
||||||
# and it auto-extends tasks/handlers/vars_files/module paths as appropriate if found
|
|
||||||
|
|
||||||
if roles is None:
|
|
||||||
roles = []
|
|
||||||
if type(roles) != list:
|
|
||||||
raise errors.AnsibleError("value of 'roles:' must be a list")
|
|
||||||
|
|
||||||
new_tasks = []
|
|
||||||
new_handlers = []
|
|
||||||
role_vars_files = []
|
|
||||||
defaults_files = []
|
|
||||||
|
|
||||||
pre_tasks = ds.get('pre_tasks', None)
|
|
||||||
if type(pre_tasks) != list:
|
|
||||||
pre_tasks = []
|
|
||||||
for x in pre_tasks:
|
|
||||||
new_tasks.append(x)
|
|
||||||
|
|
||||||
# flush handlers after pre_tasks
|
|
||||||
new_tasks.append(dict(meta='flush_handlers'))
|
|
||||||
|
|
||||||
roles = self._build_role_dependencies(roles, [], {})
|
|
||||||
|
|
||||||
# give each role an uuid and
|
|
||||||
# make role_path available as variable to the task
|
|
||||||
for idx, val in enumerate(roles):
|
|
||||||
this_uuid = str(uuid.uuid4())
|
|
||||||
roles[idx][-3]['role_uuid'] = this_uuid
|
|
||||||
roles[idx][-3]['role_path'] = roles[idx][1]
|
|
||||||
|
|
||||||
role_names = []
|
|
||||||
|
|
||||||
for (role, role_path, role_vars, role_params, default_vars) in roles:
|
|
||||||
# special vars must be extracted from the dict to the included tasks
|
|
||||||
special_keys = [ "sudo", "sudo_user", "when", "with_items", "su", "su_user", "become", "become_user" ]
|
|
||||||
special_vars = {}
|
|
||||||
for k in special_keys:
|
|
||||||
if k in role_vars:
|
|
||||||
special_vars[k] = role_vars[k]
|
|
||||||
|
|
||||||
task_basepath = utils.path_dwim(self.basedir, os.path.join(role_path, 'tasks'))
|
|
||||||
handler_basepath = utils.path_dwim(self.basedir, os.path.join(role_path, 'handlers'))
|
|
||||||
vars_basepath = utils.path_dwim(self.basedir, os.path.join(role_path, 'vars'))
|
|
||||||
meta_basepath = utils.path_dwim(self.basedir, os.path.join(role_path, 'meta'))
|
|
||||||
defaults_basepath = utils.path_dwim(self.basedir, os.path.join(role_path, 'defaults'))
|
|
||||||
|
|
||||||
task = self._resolve_main(task_basepath)
|
|
||||||
handler = self._resolve_main(handler_basepath)
|
|
||||||
vars_file = self._resolve_main(vars_basepath)
|
|
||||||
meta_file = self._resolve_main(meta_basepath)
|
|
||||||
defaults_file = self._resolve_main(defaults_basepath)
|
|
||||||
|
|
||||||
library = utils.path_dwim(self.basedir, os.path.join(role_path, 'library'))
|
|
||||||
|
|
||||||
missing = lambda f: not os.path.isfile(f)
|
|
||||||
if missing(task) and missing(handler) and missing(vars_file) and missing(defaults_file) and missing(meta_file) and not os.path.isdir(library):
|
|
||||||
raise errors.AnsibleError("found role at %s, but cannot find %s or %s or %s or %s or %s or %s" % (role_path, task, handler, vars_file, defaults_file, meta_file, library))
|
|
||||||
|
|
||||||
if isinstance(role, dict):
|
|
||||||
role_name = role['role']
|
|
||||||
else:
|
|
||||||
role_name = utils.role_spec_parse(role)["name"]
|
|
||||||
|
|
||||||
role_names.append(role_name)
|
|
||||||
if os.path.isfile(task):
|
|
||||||
nt = dict(include=pipes.quote(task), vars=role_vars, role_params=role_params, default_vars=default_vars, role_name=role_name)
|
|
||||||
for k in special_keys:
|
|
||||||
if k in special_vars:
|
|
||||||
nt[k] = special_vars[k]
|
|
||||||
new_tasks.append(nt)
|
|
||||||
if os.path.isfile(handler):
|
|
||||||
nt = dict(include=pipes.quote(handler), vars=role_vars, role_params=role_params, role_name=role_name)
|
|
||||||
for k in special_keys:
|
|
||||||
if k in special_vars:
|
|
||||||
nt[k] = special_vars[k]
|
|
||||||
new_handlers.append(nt)
|
|
||||||
if os.path.isfile(vars_file):
|
|
||||||
role_vars_files.append(vars_file)
|
|
||||||
if os.path.isfile(defaults_file):
|
|
||||||
defaults_files.append(defaults_file)
|
|
||||||
if os.path.isdir(library):
|
|
||||||
utils.plugins.module_finder.add_directory(library)
|
|
||||||
|
|
||||||
tasks = ds.get('tasks', None)
|
|
||||||
post_tasks = ds.get('post_tasks', None)
|
|
||||||
handlers = ds.get('handlers', None)
|
|
||||||
vars_files = ds.get('vars_files', None)
|
|
||||||
|
|
||||||
if type(tasks) != list:
|
|
||||||
tasks = []
|
|
||||||
if type(handlers) != list:
|
|
||||||
handlers = []
|
|
||||||
if type(vars_files) != list:
|
|
||||||
vars_files = []
|
|
||||||
if type(post_tasks) != list:
|
|
||||||
post_tasks = []
|
|
||||||
|
|
||||||
new_tasks.extend(tasks)
|
|
||||||
# flush handlers after tasks + role tasks
|
|
||||||
new_tasks.append(dict(meta='flush_handlers'))
|
|
||||||
new_tasks.extend(post_tasks)
|
|
||||||
# flush handlers after post tasks
|
|
||||||
new_tasks.append(dict(meta='flush_handlers'))
|
|
||||||
|
|
||||||
new_handlers.extend(handlers)
|
|
||||||
|
|
||||||
ds['tasks'] = new_tasks
|
|
||||||
ds['handlers'] = new_handlers
|
|
||||||
ds['role_names'] = role_names
|
|
||||||
|
|
||||||
self.role_vars = self._load_role_vars_files(role_vars_files)
|
|
||||||
self.default_vars = self._load_role_defaults(defaults_files)
|
|
||||||
|
|
||||||
return ds
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def _resolve_main(self, basepath):
|
|
||||||
''' flexibly handle variations in main filenames '''
|
|
||||||
# these filenames are acceptable:
|
|
||||||
mains = (
|
|
||||||
os.path.join(basepath, 'main'),
|
|
||||||
os.path.join(basepath, 'main.yml'),
|
|
||||||
os.path.join(basepath, 'main.yaml'),
|
|
||||||
os.path.join(basepath, 'main.json'),
|
|
||||||
)
|
|
||||||
if sum([os.path.isfile(x) for x in mains]) > 1:
|
|
||||||
raise errors.AnsibleError("found multiple main files at %s, only one allowed" % (basepath))
|
|
||||||
else:
|
|
||||||
for m in mains:
|
|
||||||
if os.path.isfile(m):
|
|
||||||
return m # exactly one main file
|
|
||||||
return mains[0] # zero mains (we still need to return something)
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def _load_tasks(self, tasks, vars=None, role_params=None, default_vars=None, become_vars=None,
|
|
||||||
additional_conditions=None, original_file=None, role_name=None):
|
|
||||||
''' handle task and handler include statements '''
|
|
||||||
|
|
||||||
results = []
|
|
||||||
if tasks is None:
|
|
||||||
# support empty handler files, and the like.
|
|
||||||
tasks = []
|
|
||||||
if additional_conditions is None:
|
|
||||||
additional_conditions = []
|
|
||||||
if vars is None:
|
|
||||||
vars = {}
|
|
||||||
if role_params is None:
|
|
||||||
role_params = {}
|
|
||||||
if default_vars is None:
|
|
||||||
default_vars = {}
|
|
||||||
if become_vars is None:
|
|
||||||
become_vars = {}
|
|
||||||
|
|
||||||
old_conditions = list(additional_conditions)
|
|
||||||
|
|
||||||
for x in tasks:
|
|
||||||
|
|
||||||
# prevent assigning the same conditions to each task on an include
|
|
||||||
included_additional_conditions = list(old_conditions)
|
|
||||||
|
|
||||||
if not isinstance(x, dict):
|
|
||||||
raise errors.AnsibleError("expecting dict; got: %s, error in %s" % (x, original_file))
|
|
||||||
|
|
||||||
# evaluate privilege escalation vars for current and child tasks
|
|
||||||
included_become_vars = {}
|
|
||||||
for k in ["become", "become_user", "become_method", "become_exe", "sudo", "su", "sudo_user", "su_user"]:
|
|
||||||
if k in x:
|
|
||||||
included_become_vars[k] = x[k]
|
|
||||||
elif k in become_vars:
|
|
||||||
included_become_vars[k] = become_vars[k]
|
|
||||||
x[k] = become_vars[k]
|
|
||||||
|
|
||||||
task_vars = vars.copy()
|
|
||||||
if original_file:
|
|
||||||
task_vars['_original_file'] = original_file
|
|
||||||
|
|
||||||
if 'meta' in x:
|
|
||||||
if x['meta'] == 'flush_handlers':
|
|
||||||
if role_name and 'role_name' not in x:
|
|
||||||
x['role_name'] = role_name
|
|
||||||
results.append(Task(self, x, module_vars=task_vars, role_name=role_name))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if 'include' in x:
|
|
||||||
tokens = split_args(str(x['include']))
|
|
||||||
included_additional_conditions = list(additional_conditions)
|
|
||||||
include_vars = {}
|
|
||||||
for k in x:
|
|
||||||
if k.startswith("with_"):
|
|
||||||
if original_file:
|
|
||||||
offender = " (in %s)" % original_file
|
|
||||||
else:
|
|
||||||
offender = ""
|
|
||||||
utils.deprecated("include + with_items is a removed deprecated feature" + offender, "1.5", removed=True)
|
|
||||||
elif k.startswith("when_"):
|
|
||||||
utils.deprecated("\"when_<criteria>:\" is a removed deprecated feature, use the simplified 'when:' conditional directly", None, removed=True)
|
|
||||||
elif k == 'when':
|
|
||||||
if isinstance(x[k], (basestring, bool)):
|
|
||||||
included_additional_conditions.append(x[k])
|
|
||||||
elif type(x[k]) is list:
|
|
||||||
included_additional_conditions.extend(x[k])
|
|
||||||
elif k in ("include", "vars", "role_params", "default_vars", "sudo", "sudo_user", "role_name", "no_log", "become", "become_user", "su", "su_user"):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
include_vars[k] = x[k]
|
|
||||||
|
|
||||||
# get any role parameters specified
|
|
||||||
role_params = x.get('role_params', {})
|
|
||||||
|
|
||||||
# get any role default variables specified
|
|
||||||
default_vars = x.get('default_vars', {})
|
|
||||||
if not default_vars:
|
|
||||||
default_vars = self.default_vars
|
|
||||||
else:
|
|
||||||
default_vars = utils.combine_vars(self.default_vars, default_vars)
|
|
||||||
|
|
||||||
# append the vars defined with the include (from above)
|
|
||||||
# as well as the old-style 'vars' element. The old-style
|
|
||||||
# vars are given higher precedence here (just in case)
|
|
||||||
task_vars = utils.combine_vars(task_vars, include_vars)
|
|
||||||
if 'vars' in x:
|
|
||||||
task_vars = utils.combine_vars(task_vars, x['vars'])
|
|
||||||
|
|
||||||
new_role = None
|
|
||||||
if 'role_name' in x:
|
|
||||||
new_role = x['role_name']
|
|
||||||
|
|
||||||
mv = task_vars.copy()
|
|
||||||
for t in tokens[1:]:
|
|
||||||
(k,v) = t.split("=", 1)
|
|
||||||
v = unquote(v)
|
|
||||||
mv[k] = template(self.basedir, v, mv)
|
|
||||||
dirname = self.basedir
|
|
||||||
if original_file:
|
|
||||||
dirname = os.path.dirname(original_file)
|
|
||||||
|
|
||||||
# temp vars are used here to avoid trampling on the existing vars structures
|
|
||||||
temp_vars = utils.combine_vars(self.vars, self.vars_file_vars)
|
|
||||||
temp_vars = utils.combine_vars(temp_vars, mv)
|
|
||||||
temp_vars = utils.combine_vars(temp_vars, self.playbook.extra_vars)
|
|
||||||
include_file = template(dirname, tokens[0], temp_vars)
|
|
||||||
include_filename = utils.path_dwim(dirname, include_file)
|
|
||||||
|
|
||||||
data = utils.parse_yaml_from_file(include_filename, vault_password=self.vault_password)
|
|
||||||
if 'role_name' in x and data is not None:
|
|
||||||
for y in data:
|
|
||||||
if isinstance(y, dict) and 'include' in y:
|
|
||||||
y['role_name'] = new_role
|
|
||||||
loaded = self._load_tasks(data, mv, role_params, default_vars, included_become_vars, list(included_additional_conditions), original_file=include_filename, role_name=new_role)
|
|
||||||
results += loaded
|
|
||||||
elif type(x) == dict:
|
|
||||||
task = Task(
|
|
||||||
self, x,
|
|
||||||
module_vars=task_vars,
|
|
||||||
play_vars=self.vars,
|
|
||||||
play_file_vars=self.vars_file_vars,
|
|
||||||
role_vars=self.role_vars,
|
|
||||||
role_params=role_params,
|
|
||||||
default_vars=default_vars,
|
|
||||||
additional_conditions=list(additional_conditions),
|
|
||||||
role_name=role_name
|
|
||||||
)
|
|
||||||
results.append(task)
|
|
||||||
else:
|
|
||||||
raise Exception("unexpected task type")
|
|
||||||
|
|
||||||
for x in results:
|
|
||||||
if self.tags is not None:
|
|
||||||
x.tags.extend(self.tags)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def tasks(self):
|
|
||||||
''' return task objects for this play '''
|
|
||||||
return self._tasks
|
|
||||||
|
|
||||||
def handlers(self):
|
|
||||||
''' return handler objects for this play '''
|
|
||||||
return self._handlers
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def _get_vars(self):
|
|
||||||
''' load the vars section from a play, accounting for all sorts of variable features
|
|
||||||
including loading from yaml files, prompting, and conditional includes of the first
|
|
||||||
file found in a list. '''
|
|
||||||
|
|
||||||
if self.vars is None:
|
|
||||||
self.vars = {}
|
|
||||||
|
|
||||||
if type(self.vars) not in [dict, list]:
|
|
||||||
raise errors.AnsibleError("'vars' section must contain only key/value pairs")
|
|
||||||
|
|
||||||
vars = {}
|
|
||||||
|
|
||||||
# translate a list of vars into a dict
|
|
||||||
if type(self.vars) == list:
|
|
||||||
for item in self.vars:
|
|
||||||
if getattr(item, 'items', None) is None:
|
|
||||||
raise errors.AnsibleError("expecting a key-value pair in 'vars' section")
|
|
||||||
k, v = item.items()[0]
|
|
||||||
vars[k] = v
|
|
||||||
else:
|
|
||||||
vars.update(self.vars)
|
|
||||||
|
|
||||||
if type(self.vars_prompt) == list:
|
|
||||||
for var in self.vars_prompt:
|
|
||||||
if not 'name' in var:
|
|
||||||
raise errors.AnsibleError("'vars_prompt' item is missing 'name:'")
|
|
||||||
|
|
||||||
vname = var['name']
|
|
||||||
prompt = var.get("prompt", vname)
|
|
||||||
default = var.get("default", None)
|
|
||||||
private = var.get("private", True)
|
|
||||||
|
|
||||||
confirm = var.get("confirm", False)
|
|
||||||
encrypt = var.get("encrypt", None)
|
|
||||||
salt_size = var.get("salt_size", None)
|
|
||||||
salt = var.get("salt", None)
|
|
||||||
|
|
||||||
if vname not in self.playbook.extra_vars:
|
|
||||||
vars[vname] = self.playbook.callbacks.on_vars_prompt(
|
|
||||||
vname, private, prompt, encrypt, confirm, salt_size, salt, default
|
|
||||||
)
|
|
||||||
|
|
||||||
elif type(self.vars_prompt) == dict:
|
|
||||||
for (vname, prompt) in self.vars_prompt.iteritems():
|
|
||||||
prompt_msg = "%s: " % prompt
|
|
||||||
if vname not in self.playbook.extra_vars:
|
|
||||||
vars[vname] = self.playbook.callbacks.on_vars_prompt(
|
|
||||||
varname=vname, private=False, prompt=prompt_msg, default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleError("'vars_prompt' section is malformed, see docs")
|
|
||||||
|
|
||||||
if type(self.playbook.extra_vars) == dict:
|
|
||||||
vars = utils.combine_vars(vars, self.playbook.extra_vars)
|
|
||||||
|
|
||||||
return vars
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def update_vars_files(self, hosts, vault_password=None):
|
|
||||||
''' calculate vars_files, which requires that setup runs first so ansible facts can be mixed in '''
|
|
||||||
|
|
||||||
# now loop through all the hosts...
|
|
||||||
for h in hosts:
|
|
||||||
self._update_vars_files_for_host(h, vault_password=vault_password)
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def compare_tags(self, tags):
|
|
||||||
''' given a list of tags that the user has specified, return two lists:
|
|
||||||
matched_tags: tags were found within the current play and match those given
|
|
||||||
by the user
|
|
||||||
unmatched_tags: tags that were found within the current play but do not match
|
|
||||||
any provided by the user '''
|
|
||||||
|
|
||||||
# gather all the tags in all the tasks and handlers into one list
|
|
||||||
# FIXME: isn't this in self.tags already?
|
|
||||||
|
|
||||||
all_tags = []
|
|
||||||
for task in self._tasks:
|
|
||||||
if not task.meta:
|
|
||||||
all_tags.extend(task.tags)
|
|
||||||
for handler in self._handlers:
|
|
||||||
all_tags.extend(handler.tags)
|
|
||||||
|
|
||||||
# compare the lists of tags using sets and return the matched and unmatched
|
|
||||||
all_tags_set = set(all_tags)
|
|
||||||
tags_set = set(tags)
|
|
||||||
|
|
||||||
matched_tags = all_tags_set.intersection(tags_set)
|
|
||||||
unmatched_tags = all_tags_set.difference(tags_set)
|
|
||||||
|
|
||||||
a = set(['always'])
|
|
||||||
u = set(['untagged'])
|
|
||||||
if 'always' in all_tags_set:
|
|
||||||
matched_tags = matched_tags.union(a)
|
|
||||||
unmatched_tags = all_tags_set.difference(a)
|
|
||||||
|
|
||||||
if 'all' in tags_set:
|
|
||||||
matched_tags = matched_tags.union(all_tags_set)
|
|
||||||
unmatched_tags = set()
|
|
||||||
|
|
||||||
if 'tagged' in tags_set:
|
|
||||||
matched_tags = all_tags_set.difference(u)
|
|
||||||
unmatched_tags = u
|
|
||||||
|
|
||||||
if 'untagged' in tags_set and 'untagged' in all_tags_set:
|
|
||||||
matched_tags = matched_tags.union(u)
|
|
||||||
unmatched_tags = unmatched_tags.difference(u)
|
|
||||||
|
|
||||||
return matched_tags, unmatched_tags
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def _late_merge_role_tags(self):
|
|
||||||
# build a local dict of tags for roles
|
|
||||||
role_tags = {}
|
|
||||||
for task in self._ds['tasks']:
|
|
||||||
if 'role_name' in task:
|
|
||||||
this_role = task['role_name'] + "-" + task['vars']['role_uuid']
|
|
||||||
|
|
||||||
if this_role not in role_tags:
|
|
||||||
role_tags[this_role] = []
|
|
||||||
|
|
||||||
if 'tags' in task['vars']:
|
|
||||||
if isinstance(task['vars']['tags'], basestring):
|
|
||||||
role_tags[this_role] += shlex.split(task['vars']['tags'])
|
|
||||||
else:
|
|
||||||
role_tags[this_role] += task['vars']['tags']
|
|
||||||
|
|
||||||
# apply each role's tags to its tasks
|
|
||||||
for idx, val in enumerate(self._tasks):
|
|
||||||
if getattr(val, 'role_name', None) is not None:
|
|
||||||
this_role = val.role_name + "-" + val.module_vars['role_uuid']
|
|
||||||
if this_role in role_tags:
|
|
||||||
self._tasks[idx].tags = sorted(set(self._tasks[idx].tags + role_tags[this_role]))
|
|
||||||
|
|
||||||
# *************************************************
|
|
||||||
|
|
||||||
def _update_vars_files_for_host(self, host, vault_password=None):
|
|
||||||
|
|
||||||
def generate_filenames(host, inject, filename):
|
|
||||||
|
|
||||||
""" Render the raw filename into 3 forms """
|
|
||||||
|
|
||||||
# filename2 is the templated version of the filename, which will
|
|
||||||
# be fully rendered if any variables contained within it are
|
|
||||||
# non-inventory related
|
|
||||||
filename2 = template(self.basedir, filename, self.vars)
|
|
||||||
|
|
||||||
# filename3 is the same as filename2, but when the host object is
|
|
||||||
# available, inventory variables will be expanded as well since the
|
|
||||||
# name is templated with the injected variables
|
|
||||||
filename3 = filename2
|
|
||||||
if host is not None:
|
|
||||||
filename3 = template(self.basedir, filename2, inject)
|
|
||||||
|
|
||||||
# filename4 is the dwim'd path, but may also be mixed-scope, so we use
|
|
||||||
# both play scoped vars and host scoped vars to template the filepath
|
|
||||||
if utils.contains_vars(filename3) and host is not None:
|
|
||||||
inject.update(self.vars)
|
|
||||||
filename4 = template(self.basedir, filename3, inject)
|
|
||||||
filename4 = utils.path_dwim(self.basedir, filename4)
|
|
||||||
else:
|
|
||||||
filename4 = utils.path_dwim(self.basedir, filename3)
|
|
||||||
|
|
||||||
return filename2, filename3, filename4
|
|
||||||
|
|
||||||
|
|
||||||
def update_vars_cache(host, data, target_filename=None):
|
|
||||||
|
|
||||||
""" update a host's varscache with new var data """
|
|
||||||
|
|
||||||
self.playbook.VARS_CACHE[host] = utils.combine_vars(self.playbook.VARS_CACHE.get(host, {}), data)
|
|
||||||
if target_filename:
|
|
||||||
self.playbook.callbacks.on_import_for_host(host, target_filename)
|
|
||||||
|
|
||||||
def process_files(filename, filename2, filename3, filename4, host=None):
|
|
||||||
|
|
||||||
""" pseudo-algorithm for deciding where new vars should go """
|
|
||||||
|
|
||||||
data = utils.parse_yaml_from_file(filename4, vault_password=self.vault_password)
|
|
||||||
if data:
|
|
||||||
if type(data) != dict:
|
|
||||||
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % filename4)
|
|
||||||
if host is not None:
|
|
||||||
target_filename = None
|
|
||||||
if utils.contains_vars(filename2):
|
|
||||||
if not utils.contains_vars(filename3):
|
|
||||||
target_filename = filename3
|
|
||||||
else:
|
|
||||||
target_filename = filename4
|
|
||||||
update_vars_cache(host, data, target_filename=target_filename)
|
|
||||||
else:
|
|
||||||
self.vars_file_vars = utils.combine_vars(self.vars_file_vars, data)
|
|
||||||
# we did process this file
|
|
||||||
return True
|
|
||||||
# we did not process this file
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Enforce that vars_files is always a list
|
|
||||||
if type(self.vars_files) != list:
|
|
||||||
self.vars_files = [ self.vars_files ]
|
|
||||||
|
|
||||||
# Build an inject if this is a host run started by self.update_vars_files
|
|
||||||
if host is not None:
|
|
||||||
inject = {}
|
|
||||||
inject.update(self.playbook.inventory.get_variables(host, vault_password=vault_password))
|
|
||||||
inject.update(self.playbook.SETUP_CACHE.get(host, {}))
|
|
||||||
inject.update(self.playbook.VARS_CACHE.get(host, {}))
|
|
||||||
else:
|
|
||||||
inject = None
|
|
||||||
|
|
||||||
processed = []
|
|
||||||
for filename in self.vars_files:
|
|
||||||
if type(filename) == list:
|
|
||||||
# loop over all filenames, loading the first one, and failing if none found
|
|
||||||
found = False
|
|
||||||
sequence = []
|
|
||||||
for real_filename in filename:
|
|
||||||
filename2, filename3, filename4 = generate_filenames(host, inject, real_filename)
|
|
||||||
sequence.append(filename4)
|
|
||||||
if os.path.exists(filename4):
|
|
||||||
found = True
|
|
||||||
if process_files(filename, filename2, filename3, filename4, host=host):
|
|
||||||
processed.append(filename)
|
|
||||||
elif host is not None:
|
|
||||||
self.playbook.callbacks.on_not_import_for_host(host, filename4)
|
|
||||||
if found:
|
|
||||||
break
|
|
||||||
if not found and host is not None:
|
|
||||||
raise errors.AnsibleError(
|
|
||||||
"%s: FATAL, no files matched for vars_files import sequence: %s" % (host, sequence)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# just one filename supplied, load it!
|
|
||||||
filename2, filename3, filename4 = generate_filenames(host, inject, filename)
|
|
||||||
if utils.contains_vars(filename4):
|
|
||||||
continue
|
|
||||||
if process_files(filename, filename2, filename3, filename4, host=host):
|
|
||||||
processed.append(filename)
|
|
||||||
|
|
||||||
return processed
|
|
@ -1,346 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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 import errors
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.module_utils.splitter import split_args
|
|
||||||
import os
|
|
||||||
import ansible.utils.template as template
|
|
||||||
import sys
|
|
||||||
|
|
||||||
class Task(object):
|
|
||||||
|
|
||||||
_t_common = [
|
|
||||||
'action', 'always_run', 'any_errors_fatal', 'args', 'become', 'become_method', 'become_pass',
|
|
||||||
'become_user', 'changed_when', 'delay', 'delegate_to', 'environment', 'failed_when',
|
|
||||||
'first_available_file', 'ignore_errors', 'local_action', 'meta', 'name', 'no_log',
|
|
||||||
'notify', 'register', 'remote_user', 'retries', 'run_once', 'su', 'su_pass', 'su_user',
|
|
||||||
'sudo', 'sudo_pass', 'sudo_user', 'tags', 'transport', 'until', 'when',
|
|
||||||
]
|
|
||||||
|
|
||||||
__slots__ = [
|
|
||||||
'async_poll_interval', 'async_seconds', 'default_vars', 'first_available_file',
|
|
||||||
'items_lookup_plugin', 'items_lookup_terms', 'module_args', 'module_name', 'module_vars',
|
|
||||||
'notified_by', 'play', 'play_file_vars', 'play_vars', 'role_name', 'role_params', 'role_vars',
|
|
||||||
] + _t_common
|
|
||||||
|
|
||||||
# to prevent typos and such
|
|
||||||
VALID_KEYS = frozenset([
|
|
||||||
'async', 'connection', 'include', 'poll',
|
|
||||||
] + _t_common)
|
|
||||||
|
|
||||||
def __init__(self, play, ds, module_vars=None, play_vars=None, play_file_vars=None, role_vars=None, role_params=None, default_vars=None, additional_conditions=None, role_name=None):
|
|
||||||
''' constructor loads from a task or handler datastructure '''
|
|
||||||
|
|
||||||
# meta directives are used to tell things like ansible/playbook to run
|
|
||||||
# operations like handler execution. Meta tasks are not executed
|
|
||||||
# normally.
|
|
||||||
if 'meta' in ds:
|
|
||||||
self.meta = ds['meta']
|
|
||||||
self.tags = []
|
|
||||||
self.module_vars = module_vars
|
|
||||||
self.role_name = role_name
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.meta = None
|
|
||||||
|
|
||||||
|
|
||||||
library = os.path.join(play.basedir, 'library')
|
|
||||||
if os.path.exists(library):
|
|
||||||
utils.plugins.module_finder.add_directory(library)
|
|
||||||
|
|
||||||
for x in ds.keys():
|
|
||||||
|
|
||||||
# code to allow for saying "modulename: args" versus "action: modulename args"
|
|
||||||
if x in utils.plugins.module_finder:
|
|
||||||
|
|
||||||
if 'action' in ds:
|
|
||||||
raise errors.AnsibleError("multiple actions specified in task: '%s' and '%s'" % (x, ds.get('name', ds['action'])))
|
|
||||||
if isinstance(ds[x], dict):
|
|
||||||
if 'args' in ds:
|
|
||||||
raise errors.AnsibleError("can't combine args: and a dict for %s: in task %s" % (x, ds.get('name', "%s: %s" % (x, ds[x]))))
|
|
||||||
ds['args'] = ds[x]
|
|
||||||
ds[x] = ''
|
|
||||||
elif ds[x] is None:
|
|
||||||
ds[x] = ''
|
|
||||||
if not isinstance(ds[x], basestring):
|
|
||||||
raise errors.AnsibleError("action specified for task %s has invalid type %s" % (ds.get('name', "%s: %s" % (x, ds[x])), type(ds[x])))
|
|
||||||
ds['action'] = x + " " + ds[x]
|
|
||||||
ds.pop(x)
|
|
||||||
|
|
||||||
# code to allow "with_glob" and to reference a lookup plugin named glob
|
|
||||||
elif x.startswith("with_"):
|
|
||||||
if isinstance(ds[x], basestring):
|
|
||||||
param = ds[x].strip()
|
|
||||||
|
|
||||||
plugin_name = x.replace("with_","")
|
|
||||||
if plugin_name in utils.plugins.lookup_loader:
|
|
||||||
ds['items_lookup_plugin'] = plugin_name
|
|
||||||
ds['items_lookup_terms'] = ds[x]
|
|
||||||
ds.pop(x)
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleError("cannot find lookup plugin named %s for usage in with_%s" % (plugin_name, plugin_name))
|
|
||||||
|
|
||||||
elif x in [ 'changed_when', 'failed_when', 'when']:
|
|
||||||
if isinstance(ds[x], basestring):
|
|
||||||
param = ds[x].strip()
|
|
||||||
# Only a variable, no logic
|
|
||||||
if (param.startswith('{{') and
|
|
||||||
param.find('}}') == len(ds[x]) - 2 and
|
|
||||||
param.find('|') == -1):
|
|
||||||
utils.warning("It is unnecessary to use '{{' in conditionals, leave variables in loop expressions bare.")
|
|
||||||
elif x.startswith("when_"):
|
|
||||||
utils.deprecated("The 'when_' conditional has been removed. Switch to using the regular unified 'when' statements as described on docs.ansible.com.","1.5", removed=True)
|
|
||||||
|
|
||||||
if 'when' in ds:
|
|
||||||
raise errors.AnsibleError("multiple when_* statements specified in task %s" % (ds.get('name', ds['action'])))
|
|
||||||
when_name = x.replace("when_","")
|
|
||||||
ds['when'] = "%s %s" % (when_name, ds[x])
|
|
||||||
ds.pop(x)
|
|
||||||
elif not x in Task.VALID_KEYS:
|
|
||||||
raise errors.AnsibleError("%s is not a legal parameter in an Ansible task or handler" % x)
|
|
||||||
|
|
||||||
self.module_vars = module_vars
|
|
||||||
self.play_vars = play_vars
|
|
||||||
self.play_file_vars = play_file_vars
|
|
||||||
self.role_vars = role_vars
|
|
||||||
self.role_params = role_params
|
|
||||||
self.default_vars = default_vars
|
|
||||||
self.play = play
|
|
||||||
|
|
||||||
# load various attributes
|
|
||||||
self.name = ds.get('name', None)
|
|
||||||
self.tags = [ 'untagged' ]
|
|
||||||
self.register = ds.get('register', None)
|
|
||||||
self.environment = ds.get('environment', play.environment)
|
|
||||||
self.role_name = role_name
|
|
||||||
self.no_log = utils.boolean(ds.get('no_log', "false")) or self.play.no_log
|
|
||||||
self.run_once = utils.boolean(ds.get('run_once', 'false'))
|
|
||||||
|
|
||||||
#Code to allow do until feature in a Task
|
|
||||||
if 'until' in ds:
|
|
||||||
if not ds.get('register'):
|
|
||||||
raise errors.AnsibleError("register keyword is mandatory when using do until feature")
|
|
||||||
self.module_vars['delay'] = ds.get('delay', 5)
|
|
||||||
self.module_vars['retries'] = ds.get('retries', 3)
|
|
||||||
self.module_vars['register'] = ds.get('register', None)
|
|
||||||
self.until = ds.get('until')
|
|
||||||
self.module_vars['until'] = self.until
|
|
||||||
|
|
||||||
# rather than simple key=value args on the options line, these represent structured data and the values
|
|
||||||
# can be hashes and lists, not just scalars
|
|
||||||
self.args = ds.get('args', {})
|
|
||||||
|
|
||||||
# get remote_user for task, then play, then playbook
|
|
||||||
if ds.get('remote_user') is not None:
|
|
||||||
self.remote_user = ds.get('remote_user')
|
|
||||||
elif ds.get('remote_user', play.remote_user) is not None:
|
|
||||||
self.remote_user = ds.get('remote_user', play.remote_user)
|
|
||||||
else:
|
|
||||||
self.remote_user = ds.get('remote_user', play.playbook.remote_user)
|
|
||||||
|
|
||||||
# Fail out if user specifies privilege escalation params in conflict
|
|
||||||
if (ds.get('become') or ds.get('become_user') or ds.get('become_pass')) and (ds.get('sudo') or ds.get('sudo_user') or ds.get('sudo_pass')):
|
|
||||||
raise errors.AnsibleError('incompatible parameters ("become", "become_user", "become_pass") and sudo params "sudo", "sudo_user", "sudo_pass" in task: %s' % self.name)
|
|
||||||
|
|
||||||
if (ds.get('become') or ds.get('become_user') or ds.get('become_pass')) and (ds.get('su') or ds.get('su_user') or ds.get('su_pass')):
|
|
||||||
raise errors.AnsibleError('incompatible parameters ("become", "become_user", "become_pass") and su params "su", "su_user", "sudo_pass" in task: %s' % self.name)
|
|
||||||
|
|
||||||
if (ds.get('sudo') or ds.get('sudo_user') or ds.get('sudo_pass')) and (ds.get('su') or ds.get('su_user') or ds.get('su_pass')):
|
|
||||||
raise errors.AnsibleError('incompatible parameters ("su", "su_user", "su_pass") and sudo params "sudo", "sudo_user", "sudo_pass" in task: %s' % self.name)
|
|
||||||
|
|
||||||
self.become = utils.boolean(ds.get('become', play.become))
|
|
||||||
self.become_method = ds.get('become_method', play.become_method)
|
|
||||||
self.become_user = ds.get('become_user', play.become_user)
|
|
||||||
self.become_pass = ds.get('become_pass', play.playbook.become_pass)
|
|
||||||
|
|
||||||
# set only if passed in current task data
|
|
||||||
if 'sudo' in ds or 'sudo_user' in ds:
|
|
||||||
self.become_method='sudo'
|
|
||||||
|
|
||||||
if 'sudo' in ds:
|
|
||||||
self.become=ds['sudo']
|
|
||||||
del ds['sudo']
|
|
||||||
else:
|
|
||||||
self.become=True
|
|
||||||
if 'sudo_user' in ds:
|
|
||||||
self.become_user = ds['sudo_user']
|
|
||||||
del ds['sudo_user']
|
|
||||||
if 'sudo_pass' in ds:
|
|
||||||
self.become_pass = ds['sudo_pass']
|
|
||||||
del ds['sudo_pass']
|
|
||||||
|
|
||||||
elif 'su' in ds or 'su_user' in ds:
|
|
||||||
self.become_method='su'
|
|
||||||
|
|
||||||
if 'su' in ds:
|
|
||||||
self.become=ds['su']
|
|
||||||
else:
|
|
||||||
self.become=True
|
|
||||||
del ds['su']
|
|
||||||
if 'su_user' in ds:
|
|
||||||
self.become_user = ds['su_user']
|
|
||||||
del ds['su_user']
|
|
||||||
if 'su_pass' in ds:
|
|
||||||
self.become_pass = ds['su_pass']
|
|
||||||
del ds['su_pass']
|
|
||||||
|
|
||||||
# Both are defined
|
|
||||||
if ('action' in ds) and ('local_action' in ds):
|
|
||||||
raise errors.AnsibleError("the 'action' and 'local_action' attributes can not be used together")
|
|
||||||
# Both are NOT defined
|
|
||||||
elif (not 'action' in ds) and (not 'local_action' in ds):
|
|
||||||
raise errors.AnsibleError("'action' or 'local_action' attribute missing in task \"%s\"" % ds.get('name', '<Unnamed>'))
|
|
||||||
# Only one of them is defined
|
|
||||||
elif 'local_action' in ds:
|
|
||||||
self.action = ds.get('local_action', '')
|
|
||||||
self.delegate_to = '127.0.0.1'
|
|
||||||
else:
|
|
||||||
self.action = ds.get('action', '')
|
|
||||||
self.delegate_to = ds.get('delegate_to', None)
|
|
||||||
self.transport = ds.get('connection', ds.get('transport', play.transport))
|
|
||||||
|
|
||||||
if isinstance(self.action, dict):
|
|
||||||
if 'module' not in self.action:
|
|
||||||
raise errors.AnsibleError("'module' attribute missing from action in task \"%s\"" % ds.get('name', '%s' % self.action))
|
|
||||||
if self.args:
|
|
||||||
raise errors.AnsibleError("'args' cannot be combined with dict 'action' in task \"%s\"" % ds.get('name', '%s' % self.action))
|
|
||||||
self.args = self.action
|
|
||||||
self.action = self.args.pop('module')
|
|
||||||
|
|
||||||
# delegate_to can use variables
|
|
||||||
if not (self.delegate_to is None):
|
|
||||||
# delegate_to: localhost should use local transport
|
|
||||||
if self.delegate_to in ['127.0.0.1', 'localhost']:
|
|
||||||
self.transport = 'local'
|
|
||||||
|
|
||||||
# notified by is used by Playbook code to flag which hosts
|
|
||||||
# need to run a notifier
|
|
||||||
self.notified_by = []
|
|
||||||
|
|
||||||
# if no name is specified, use the action line as the name
|
|
||||||
if self.name is None:
|
|
||||||
self.name = self.action
|
|
||||||
|
|
||||||
# load various attributes
|
|
||||||
self.when = ds.get('when', None)
|
|
||||||
self.changed_when = ds.get('changed_when', None)
|
|
||||||
self.failed_when = ds.get('failed_when', None)
|
|
||||||
|
|
||||||
# combine the default and module vars here for use in templating
|
|
||||||
all_vars = self.default_vars.copy()
|
|
||||||
all_vars = utils.combine_vars(all_vars, self.play_vars)
|
|
||||||
all_vars = utils.combine_vars(all_vars, self.play_file_vars)
|
|
||||||
all_vars = utils.combine_vars(all_vars, self.role_vars)
|
|
||||||
all_vars = utils.combine_vars(all_vars, self.module_vars)
|
|
||||||
all_vars = utils.combine_vars(all_vars, self.role_params)
|
|
||||||
|
|
||||||
self.async_seconds = ds.get('async', 0) # not async by default
|
|
||||||
self.async_seconds = template.template_from_string(play.basedir, self.async_seconds, all_vars)
|
|
||||||
self.async_seconds = int(self.async_seconds)
|
|
||||||
self.async_poll_interval = ds.get('poll', 10) # default poll = 10 seconds
|
|
||||||
self.async_poll_interval = template.template_from_string(play.basedir, self.async_poll_interval, all_vars)
|
|
||||||
self.async_poll_interval = int(self.async_poll_interval)
|
|
||||||
self.notify = ds.get('notify', [])
|
|
||||||
self.first_available_file = ds.get('first_available_file', None)
|
|
||||||
|
|
||||||
self.items_lookup_plugin = ds.get('items_lookup_plugin', None)
|
|
||||||
self.items_lookup_terms = ds.get('items_lookup_terms', None)
|
|
||||||
|
|
||||||
|
|
||||||
self.ignore_errors = ds.get('ignore_errors', False)
|
|
||||||
self.any_errors_fatal = ds.get('any_errors_fatal', play.any_errors_fatal)
|
|
||||||
|
|
||||||
self.always_run = ds.get('always_run', False)
|
|
||||||
|
|
||||||
# action should be a string
|
|
||||||
if not isinstance(self.action, basestring):
|
|
||||||
raise errors.AnsibleError("action is of type '%s' and not a string in task. name: %s" % (type(self.action).__name__, self.name))
|
|
||||||
|
|
||||||
# notify can be a string or a list, store as a list
|
|
||||||
if isinstance(self.notify, basestring):
|
|
||||||
self.notify = [ self.notify ]
|
|
||||||
|
|
||||||
# split the action line into a module name + arguments
|
|
||||||
try:
|
|
||||||
tokens = split_args(self.action)
|
|
||||||
except Exception, e:
|
|
||||||
if "unbalanced" in str(e):
|
|
||||||
raise errors.AnsibleError("There was an error while parsing the task %s.\n" % repr(self.action) + \
|
|
||||||
"Make sure quotes are matched or escaped properly")
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
if len(tokens) < 1:
|
|
||||||
raise errors.AnsibleError("invalid/missing action in task. name: %s" % self.name)
|
|
||||||
self.module_name = tokens[0]
|
|
||||||
self.module_args = ''
|
|
||||||
if len(tokens) > 1:
|
|
||||||
self.module_args = " ".join(tokens[1:])
|
|
||||||
|
|
||||||
import_tags = self.module_vars.get('tags',[])
|
|
||||||
if type(import_tags) in [int,float]:
|
|
||||||
import_tags = str(import_tags)
|
|
||||||
elif type(import_tags) in [str,unicode]:
|
|
||||||
# allow the user to list comma delimited tags
|
|
||||||
import_tags = import_tags.split(",")
|
|
||||||
|
|
||||||
# handle mutually incompatible options
|
|
||||||
incompatibles = [ x for x in [ self.first_available_file, self.items_lookup_plugin ] if x is not None ]
|
|
||||||
if len(incompatibles) > 1:
|
|
||||||
raise errors.AnsibleError("with_(plugin), and first_available_file are mutually incompatible in a single task")
|
|
||||||
|
|
||||||
# make first_available_file accessible to Runner code
|
|
||||||
if self.first_available_file:
|
|
||||||
self.module_vars['first_available_file'] = self.first_available_file
|
|
||||||
# make sure that the 'item' variable is set when using
|
|
||||||
# first_available_file (issue #8220)
|
|
||||||
if 'item' not in self.module_vars:
|
|
||||||
self.module_vars['item'] = ''
|
|
||||||
|
|
||||||
if self.items_lookup_plugin is not None:
|
|
||||||
self.module_vars['items_lookup_plugin'] = self.items_lookup_plugin
|
|
||||||
self.module_vars['items_lookup_terms'] = self.items_lookup_terms
|
|
||||||
|
|
||||||
# allow runner to see delegate_to option
|
|
||||||
self.module_vars['delegate_to'] = self.delegate_to
|
|
||||||
|
|
||||||
# make some task attributes accessible to Runner code
|
|
||||||
self.module_vars['ignore_errors'] = self.ignore_errors
|
|
||||||
self.module_vars['register'] = self.register
|
|
||||||
self.module_vars['changed_when'] = self.changed_when
|
|
||||||
self.module_vars['failed_when'] = self.failed_when
|
|
||||||
self.module_vars['always_run'] = self.always_run
|
|
||||||
|
|
||||||
# tags allow certain parts of a playbook to be run without running the whole playbook
|
|
||||||
apply_tags = ds.get('tags', None)
|
|
||||||
if apply_tags is not None:
|
|
||||||
if type(apply_tags) in [ str, unicode ]:
|
|
||||||
self.tags.append(apply_tags)
|
|
||||||
elif type(apply_tags) in [ int, float ]:
|
|
||||||
self.tags.append(str(apply_tags))
|
|
||||||
elif type(apply_tags) == list:
|
|
||||||
self.tags.extend(apply_tags)
|
|
||||||
self.tags.extend(import_tags)
|
|
||||||
|
|
||||||
if len(self.tags) > 1:
|
|
||||||
self.tags.remove('untagged')
|
|
||||||
|
|
||||||
if additional_conditions:
|
|
||||||
new_conditions = additional_conditions[:]
|
|
||||||
if self.when:
|
|
||||||
new_conditions.append(self.when)
|
|
||||||
self.when = new_conditions
|
|
File diff suppressed because it is too large
Load Diff
@ -1,111 +0,0 @@
|
|||||||
# Copyright 2012, Seth Vidal <skvidal@fedoraproject.org>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import ansible
|
|
||||||
|
|
||||||
from ansible.callbacks import vv
|
|
||||||
from ansible.errors import AnsibleError as ae
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
from ansible.utils import parse_kv, combine_vars
|
|
||||||
from ansible.inventory.host import Host
|
|
||||||
from ansible.inventory.group import Group
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
''' Create inventory hosts and groups in the memory inventory'''
|
|
||||||
|
|
||||||
### We need to be able to modify the inventory
|
|
||||||
BYPASS_HOST_LOOP = True
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
if complex_args:
|
|
||||||
args.update(complex_args)
|
|
||||||
args.update(parse_kv(module_args))
|
|
||||||
if not 'hostname' in args and not 'name' in args:
|
|
||||||
raise ae("'name' is a required argument.")
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
# Parse out any hostname:port patterns
|
|
||||||
new_name = args.get('name', args.get('hostname', None))
|
|
||||||
vv("creating host via 'add_host': hostname=%s" % new_name)
|
|
||||||
|
|
||||||
if ":" in new_name:
|
|
||||||
new_name, new_port = new_name.split(":")
|
|
||||||
args['ansible_ssh_port'] = new_port
|
|
||||||
|
|
||||||
# redefine inventory and get group "all"
|
|
||||||
inventory = self.runner.inventory
|
|
||||||
allgroup = inventory.get_group('all')
|
|
||||||
|
|
||||||
# check if host in cache, add if not
|
|
||||||
if new_name in inventory._hosts_cache:
|
|
||||||
new_host = inventory._hosts_cache[new_name]
|
|
||||||
else:
|
|
||||||
new_host = Host(new_name)
|
|
||||||
# only groups can be added directly to inventory
|
|
||||||
inventory._hosts_cache[new_name] = new_host
|
|
||||||
allgroup.add_host(new_host)
|
|
||||||
|
|
||||||
groupnames = args.get('groupname', args.get('groups', args.get('group', '')))
|
|
||||||
# add it to the group if that was specified
|
|
||||||
if groupnames:
|
|
||||||
for group_name in groupnames.split(","):
|
|
||||||
group_name = group_name.strip()
|
|
||||||
if not inventory.get_group(group_name):
|
|
||||||
new_group = Group(group_name)
|
|
||||||
inventory.add_group(new_group)
|
|
||||||
new_group.vars = inventory.get_group_variables(group_name, vault_password=inventory._vault_password)
|
|
||||||
grp = inventory.get_group(group_name)
|
|
||||||
grp.add_host(new_host)
|
|
||||||
|
|
||||||
# add this host to the group cache
|
|
||||||
if inventory._groups_list is not None:
|
|
||||||
if group_name in inventory._groups_list:
|
|
||||||
if new_host.name not in inventory._groups_list[group_name]:
|
|
||||||
inventory._groups_list[group_name].append(new_host.name)
|
|
||||||
|
|
||||||
vv("added host to group via add_host module: %s" % group_name)
|
|
||||||
result['new_groups'] = groupnames.split(",")
|
|
||||||
|
|
||||||
|
|
||||||
# actually load host vars
|
|
||||||
new_host.vars = combine_vars(new_host.vars, inventory.get_host_variables(new_name, update_cached=True, vault_password=inventory._vault_password))
|
|
||||||
|
|
||||||
# Add any passed variables to the new_host
|
|
||||||
for k in args.keys():
|
|
||||||
if not k in [ 'name', 'hostname', 'groupname', 'groups' ]:
|
|
||||||
new_host.set_variable(k, args[k])
|
|
||||||
|
|
||||||
result['new_host'] = new_name
|
|
||||||
|
|
||||||
# clear pattern caching completely since it's unpredictable what
|
|
||||||
# patterns may have referenced the group
|
|
||||||
inventory.clear_pattern_cache()
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=result)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
|||||||
# (c) 2013-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# Stephen Fromm <sfromm@gmail.com>
|
|
||||||
# Brian Coca <briancoca+dev@gmail.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
|
|
||||||
|
|
||||||
import os
|
|
||||||
import os.path
|
|
||||||
import pipes
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
import base64
|
|
||||||
import re
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
TRANSFERS_FILES = True
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def _assemble_from_fragments(self, src_path, delimiter=None, compiled_regexp=None):
|
|
||||||
''' assemble a file from a directory of fragments '''
|
|
||||||
tmpfd, temp_path = tempfile.mkstemp()
|
|
||||||
tmp = os.fdopen(tmpfd,'w')
|
|
||||||
delimit_me = False
|
|
||||||
add_newline = False
|
|
||||||
|
|
||||||
for f in sorted(os.listdir(src_path)):
|
|
||||||
if compiled_regexp and not compiled_regexp.search(f):
|
|
||||||
continue
|
|
||||||
fragment = "%s/%s" % (src_path, f)
|
|
||||||
if not os.path.isfile(fragment):
|
|
||||||
continue
|
|
||||||
fragment_content = file(fragment).read()
|
|
||||||
|
|
||||||
# always put a newline between fragments if the previous fragment didn't end with a newline.
|
|
||||||
if add_newline:
|
|
||||||
tmp.write('\n')
|
|
||||||
|
|
||||||
# delimiters should only appear between fragments
|
|
||||||
if delimit_me:
|
|
||||||
if delimiter:
|
|
||||||
# un-escape anything like newlines
|
|
||||||
delimiter = delimiter.decode('unicode-escape')
|
|
||||||
tmp.write(delimiter)
|
|
||||||
# always make sure there's a newline after the
|
|
||||||
# delimiter, so lines don't run together
|
|
||||||
if delimiter[-1] != '\n':
|
|
||||||
tmp.write('\n')
|
|
||||||
|
|
||||||
tmp.write(fragment_content)
|
|
||||||
delimit_me = True
|
|
||||||
if fragment_content.endswith('\n'):
|
|
||||||
add_newline = False
|
|
||||||
else:
|
|
||||||
add_newline = True
|
|
||||||
|
|
||||||
tmp.close()
|
|
||||||
return temp_path
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
|
|
||||||
src = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
delimiter = options.get('delimiter', None)
|
|
||||||
remote_src = utils.boolean(options.get('remote_src', 'yes'))
|
|
||||||
regexp = options.get('regexp', None)
|
|
||||||
|
|
||||||
|
|
||||||
if src is None or dest is None:
|
|
||||||
result = dict(failed=True, msg="src and dest are required")
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
|
|
||||||
if remote_src:
|
|
||||||
return self.runner._execute_module(conn, tmp, 'assemble', module_args, inject=inject, complex_args=complex_args)
|
|
||||||
elif '_original_file' in inject:
|
|
||||||
src = utils.path_dwim_relative(inject['_original_file'], 'files', src, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
# the source is local, so expand it here
|
|
||||||
src = os.path.expanduser(src)
|
|
||||||
|
|
||||||
_re = None
|
|
||||||
if regexp is not None:
|
|
||||||
_re = re.compile(regexp)
|
|
||||||
|
|
||||||
# Does all work assembling the file
|
|
||||||
path = self._assemble_from_fragments(src, delimiter, _re)
|
|
||||||
|
|
||||||
path_checksum = utils.checksum_s(path)
|
|
||||||
dest = self.runner._remote_expand_user(conn, dest, tmp)
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp, dest, inject)
|
|
||||||
|
|
||||||
if path_checksum != remote_checksum:
|
|
||||||
resultant = file(path).read()
|
|
||||||
if self.runner.diff:
|
|
||||||
dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % dest, inject=inject, persist_files=True)
|
|
||||||
if 'content' in dest_result.result:
|
|
||||||
dest_contents = dest_result.result['content']
|
|
||||||
if dest_result.result['encoding'] == 'base64':
|
|
||||||
dest_contents = base64.b64decode(dest_contents)
|
|
||||||
else:
|
|
||||||
raise Exception("unknown encoding, failed: %s" % dest_result.result)
|
|
||||||
xfered = self.runner._transfer_str(conn, tmp, 'src', resultant)
|
|
||||||
|
|
||||||
# fix file permissions when the copy is done as a different user
|
|
||||||
if self.runner.become and self.runner.become_user != 'root':
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
|
|
||||||
|
|
||||||
# run the copy module
|
|
||||||
new_module_args = dict(
|
|
||||||
src=xfered,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=os.path.basename(src),
|
|
||||||
)
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=src, after=resultant))
|
|
||||||
else:
|
|
||||||
res = self.runner._execute_module(conn, tmp, 'copy', module_args_tmp, inject=inject)
|
|
||||||
res.diff = dict(after=resultant)
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
new_module_args = dict(
|
|
||||||
src=xfered,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=os.path.basename(src),
|
|
||||||
)
|
|
||||||
|
|
||||||
# make sure checkmod is passed on correctly
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
return self.runner._execute_module(conn, tmp, 'file', module_args_tmp, inject=inject)
|
|
@ -1,64 +0,0 @@
|
|||||||
# Copyright 2012, Dag Wieers <dag@wieers.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/>.
|
|
||||||
|
|
||||||
import ansible
|
|
||||||
|
|
||||||
from ansible import utils, errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
''' Fail with custom message '''
|
|
||||||
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
# note: the fail module does not need to pay attention to check mode
|
|
||||||
# it always runs.
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
if complex_args:
|
|
||||||
args.update(complex_args)
|
|
||||||
args.update(utils.parse_kv(module_args))
|
|
||||||
|
|
||||||
msg = None
|
|
||||||
if 'msg' in args:
|
|
||||||
msg = args['msg']
|
|
||||||
|
|
||||||
if not 'that' in args:
|
|
||||||
raise errors.AnsibleError('conditional required in "that" string')
|
|
||||||
|
|
||||||
if not isinstance(args['that'], list):
|
|
||||||
args['that'] = [ args['that'] ]
|
|
||||||
|
|
||||||
for that in args['that']:
|
|
||||||
test_result = utils.check_conditional(that, self.runner.basedir, inject, fail_on_undefined=True)
|
|
||||||
if not test_result:
|
|
||||||
result = dict(
|
|
||||||
failed = True,
|
|
||||||
evaluated_to = test_result,
|
|
||||||
assertion = that,
|
|
||||||
)
|
|
||||||
if msg:
|
|
||||||
result['msg'] = msg
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, result=dict(msg='all assertions passed'))
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' transfer the given module name, plus the async module, then run it '''
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
|
||||||
|
|
||||||
# shell and command module are the same
|
|
||||||
if module_name == 'shell':
|
|
||||||
module_name = 'command'
|
|
||||||
module_args += " #USE_SHELL"
|
|
||||||
|
|
||||||
if "tmp" not in tmp:
|
|
||||||
tmp = self.runner._make_tmp_path(conn)
|
|
||||||
|
|
||||||
(module_path, is_new_style, shebang) = self.runner._copy_module(conn, tmp, module_name, module_args, inject, complex_args=complex_args)
|
|
||||||
self.runner._remote_chmod(conn, 'a+rx', module_path, tmp)
|
|
||||||
|
|
||||||
return self.runner._execute_module(conn, tmp, 'async_wrapper', module_args,
|
|
||||||
async_module=module_path,
|
|
||||||
async_jid=self.runner.generated_jid,
|
|
||||||
async_limit=self.runner.background,
|
|
||||||
inject=inject
|
|
||||||
)
|
|
||||||
|
|
@ -1,381 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
import ansible.constants as C
|
|
||||||
import ansible.utils.template as template
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import stat
|
|
||||||
import tempfile
|
|
||||||
import pipes
|
|
||||||
|
|
||||||
## fixes https://github.com/ansible/ansible/issues/3518
|
|
||||||
# http://mypy.pythonblogs.com/12_mypy/archive/1253_workaround_for_python_bug_ascii_codec_cant_encode_character_uxa0_in_position_111_ordinal_not_in_range128.html
|
|
||||||
import sys
|
|
||||||
reload(sys)
|
|
||||||
sys.setdefaultencoding("utf8")
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp_path, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for file transfer operations '''
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
source = options.get('src', None)
|
|
||||||
content = options.get('content', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
raw = utils.boolean(options.get('raw', 'no'))
|
|
||||||
force = utils.boolean(options.get('force', 'yes'))
|
|
||||||
|
|
||||||
# content with newlines is going to be escaped to safely load in yaml
|
|
||||||
# now we need to unescape it so that the newlines are evaluated properly
|
|
||||||
# when writing the file to disk
|
|
||||||
if content:
|
|
||||||
if isinstance(content, unicode):
|
|
||||||
try:
|
|
||||||
content = content.decode('unicode-escape')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if (source is None and content is None and not 'first_available_file' in inject) or dest is None:
|
|
||||||
result=dict(failed=True, msg="src (or content) and dest are required")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
elif (source is not None or 'first_available_file' in inject) and content is not None:
|
|
||||||
result=dict(failed=True, msg="src and content are mutually exclusive")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
# Check if the source ends with a "/"
|
|
||||||
source_trailing_slash = False
|
|
||||||
if source:
|
|
||||||
source_trailing_slash = source.endswith("/")
|
|
||||||
|
|
||||||
# Define content_tempfile in case we set it after finding content populated.
|
|
||||||
content_tempfile = None
|
|
||||||
|
|
||||||
# If content is defined make a temp file and write the content into it.
|
|
||||||
if content is not None:
|
|
||||||
try:
|
|
||||||
# If content comes to us as a dict it should be decoded json.
|
|
||||||
# We need to encode it back into a string to write it out.
|
|
||||||
if type(content) is dict:
|
|
||||||
content_tempfile = self._create_content_tempfile(json.dumps(content))
|
|
||||||
else:
|
|
||||||
content_tempfile = self._create_content_tempfile(content)
|
|
||||||
source = content_tempfile
|
|
||||||
except Exception, err:
|
|
||||||
result = dict(failed=True, msg="could not write content temp file: %s" % err)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
# if we have first_available_file in our vars
|
|
||||||
# look up the files and use the first one we find as src
|
|
||||||
elif 'first_available_file' in inject:
|
|
||||||
found = False
|
|
||||||
for fn in inject.get('first_available_file'):
|
|
||||||
fn_orig = fn
|
|
||||||
fnt = template.template(self.runner.basedir, fn, inject)
|
|
||||||
fnd = utils.path_dwim(self.runner.basedir, fnt)
|
|
||||||
if not os.path.exists(fnd) and '_original_file' in inject:
|
|
||||||
fnd = utils.path_dwim_relative(inject['_original_file'], 'files', fnt, self.runner.basedir, check=False)
|
|
||||||
if os.path.exists(fnd):
|
|
||||||
source = fnd
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
results = dict(failed=True, msg="could not find src in first_available_file list")
|
|
||||||
return ReturnData(conn=conn, result=results)
|
|
||||||
else:
|
|
||||||
source = template.template(self.runner.basedir, source, inject)
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'files', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
# A list of source file tuples (full_path, relative_path) which will try to copy to the destination
|
|
||||||
source_files = []
|
|
||||||
|
|
||||||
# If source is a directory populate our list else source is a file and translate it to a tuple.
|
|
||||||
if os.path.isdir(source):
|
|
||||||
# Get the amount of spaces to remove to get the relative path.
|
|
||||||
if source_trailing_slash:
|
|
||||||
sz = len(source) + 1
|
|
||||||
else:
|
|
||||||
sz = len(source.rsplit('/', 1)[0]) + 1
|
|
||||||
|
|
||||||
# Walk the directory and append the file tuples to source_files.
|
|
||||||
for base_path, sub_folders, files in os.walk(source):
|
|
||||||
for file in files:
|
|
||||||
full_path = os.path.join(base_path, file)
|
|
||||||
rel_path = full_path[sz:]
|
|
||||||
source_files.append((full_path, rel_path))
|
|
||||||
|
|
||||||
# If it's recursive copy, destination is always a dir,
|
|
||||||
# explicitly mark it so (note - copy module relies on this).
|
|
||||||
if not conn.shell.path_has_trailing_slash(dest):
|
|
||||||
dest = conn.shell.join_path(dest, '')
|
|
||||||
else:
|
|
||||||
source_files.append((source, os.path.basename(source)))
|
|
||||||
|
|
||||||
changed = False
|
|
||||||
diffs = []
|
|
||||||
module_result = {"changed": False}
|
|
||||||
|
|
||||||
# A register for if we executed a module.
|
|
||||||
# Used to cut down on command calls when not recursive.
|
|
||||||
module_executed = False
|
|
||||||
|
|
||||||
# Tell _execute_module to delete the file if there is one file.
|
|
||||||
delete_remote_tmp = (len(source_files) == 1)
|
|
||||||
|
|
||||||
# If this is a recursive action create a tmp_path that we can share as the _exec_module create is too late.
|
|
||||||
if not delete_remote_tmp:
|
|
||||||
if "-tmp-" not in tmp_path:
|
|
||||||
tmp_path = self.runner._make_tmp_path(conn)
|
|
||||||
|
|
||||||
# expand any user home dir specifier
|
|
||||||
dest = self.runner._remote_expand_user(conn, dest, tmp_path)
|
|
||||||
|
|
||||||
for source_full, source_rel in source_files:
|
|
||||||
# Generate a hash of the local file.
|
|
||||||
local_checksum = utils.checksum(source_full)
|
|
||||||
|
|
||||||
# If local_checksum is not defined we can't find the file so we should fail out.
|
|
||||||
if local_checksum is None:
|
|
||||||
result = dict(failed=True, msg="could not find src=%s" % source_full)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
# This is kind of optimization - if user told us destination is
|
|
||||||
# dir, do path manipulation right away, otherwise we still check
|
|
||||||
# for dest being a dir via remote call below.
|
|
||||||
if conn.shell.path_has_trailing_slash(dest):
|
|
||||||
dest_file = conn.shell.join_path(dest, source_rel)
|
|
||||||
else:
|
|
||||||
dest_file = conn.shell.join_path(dest)
|
|
||||||
|
|
||||||
# Attempt to get the remote checksum
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp_path, dest_file, inject)
|
|
||||||
|
|
||||||
if remote_checksum == '3':
|
|
||||||
# The remote_checksum was executed on a directory.
|
|
||||||
if content is not None:
|
|
||||||
# If source was defined as content remove the temporary file and fail out.
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
result = dict(failed=True, msg="can not use content with a dir as dest")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
else:
|
|
||||||
# Append the relative source location to the destination and retry remote_checksum
|
|
||||||
dest_file = conn.shell.join_path(dest, source_rel)
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp_path, dest_file, inject)
|
|
||||||
|
|
||||||
if remote_checksum == '4':
|
|
||||||
result = dict(msg="python isn't present on the system. Unable to compute checksum", failed=True)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
if remote_checksum != '1' and not force:
|
|
||||||
# remote_file exists so continue to next iteration.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if local_checksum != remote_checksum:
|
|
||||||
# The checksums don't match and we will change or error out.
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# Create a tmp_path if missing only if this is not recursive.
|
|
||||||
# If this is recursive we already have a tmp_path.
|
|
||||||
if delete_remote_tmp:
|
|
||||||
if "-tmp-" not in tmp_path:
|
|
||||||
tmp_path = self.runner._make_tmp_path(conn)
|
|
||||||
|
|
||||||
if self.runner.diff and not raw:
|
|
||||||
diff = self._get_diff_data(conn, tmp_path, inject, dest_file, source_full)
|
|
||||||
else:
|
|
||||||
diff = {}
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
diffs.append(diff)
|
|
||||||
changed = True
|
|
||||||
module_result = dict(changed=True)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Define a remote directory that we will copy the file to.
|
|
||||||
tmp_src = tmp_path + 'source'
|
|
||||||
|
|
||||||
if not raw:
|
|
||||||
conn.put_file(source_full, tmp_src)
|
|
||||||
else:
|
|
||||||
conn.put_file(source_full, dest_file)
|
|
||||||
|
|
||||||
# We have copied the file remotely and no longer require our content_tempfile
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
|
|
||||||
# fix file permissions when the copy is done as a different user
|
|
||||||
if self.runner.become and self.runner.become_user != 'root' and not raw:
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp_path)
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
# Continue to next iteration if raw is defined.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Run the copy module
|
|
||||||
|
|
||||||
# src and dest here come after original and override them
|
|
||||||
# we pass dest only to make sure it includes trailing slash in case of recursive copy
|
|
||||||
new_module_args = dict(
|
|
||||||
src=tmp_src,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=source_rel
|
|
||||||
)
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
if self.runner.no_log:
|
|
||||||
new_module_args['NO_LOG'] = True
|
|
||||||
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
module_return = self.runner._execute_module(conn, tmp_path, 'copy', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp)
|
|
||||||
module_executed = True
|
|
||||||
|
|
||||||
else:
|
|
||||||
# no need to transfer the file, already correct hash, but still need to call
|
|
||||||
# the file module in case we want to change attributes
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
# Continue to next iteration if raw is defined.
|
|
||||||
# self.runner._remove_tmp_path(conn, tmp_path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
tmp_src = tmp_path + source_rel
|
|
||||||
|
|
||||||
# Build temporary module_args.
|
|
||||||
new_module_args = dict(
|
|
||||||
src=tmp_src,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=source_rel
|
|
||||||
)
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
if self.runner.no_log:
|
|
||||||
new_module_args['NO_LOG'] = True
|
|
||||||
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
# Execute the file module.
|
|
||||||
module_return = self.runner._execute_module(conn, tmp_path, 'file', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp)
|
|
||||||
module_executed = True
|
|
||||||
|
|
||||||
module_result = module_return.result
|
|
||||||
if not module_result.get('checksum'):
|
|
||||||
module_result['checksum'] = local_checksum
|
|
||||||
if module_result.get('failed') == True:
|
|
||||||
return module_return
|
|
||||||
if module_result.get('changed') == True:
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# Delete tmp_path if we were recursive or if we did not execute a module.
|
|
||||||
if (not C.DEFAULT_KEEP_REMOTE_FILES and not delete_remote_tmp) \
|
|
||||||
or (not C.DEFAULT_KEEP_REMOTE_FILES and delete_remote_tmp and not module_executed):
|
|
||||||
self.runner._remove_tmp_path(conn, tmp_path)
|
|
||||||
|
|
||||||
# the file module returns the file path as 'path', but
|
|
||||||
# the copy module uses 'dest', so add it if it's not there
|
|
||||||
if 'path' in module_result and 'dest' not in module_result:
|
|
||||||
module_result['dest'] = module_result['path']
|
|
||||||
|
|
||||||
# TODO: Support detailed status/diff for multiple files
|
|
||||||
if len(source_files) == 1:
|
|
||||||
result = module_result
|
|
||||||
else:
|
|
||||||
result = dict(dest=dest, src=source, changed=changed)
|
|
||||||
if len(diffs) == 1:
|
|
||||||
return ReturnData(conn=conn, result=result, diff=diffs[0])
|
|
||||||
else:
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
def _create_content_tempfile(self, content):
|
|
||||||
''' Create a tempfile containing defined content '''
|
|
||||||
fd, content_tempfile = tempfile.mkstemp()
|
|
||||||
f = os.fdopen(fd, 'w')
|
|
||||||
try:
|
|
||||||
f.write(content)
|
|
||||||
except Exception, err:
|
|
||||||
os.remove(content_tempfile)
|
|
||||||
raise Exception(err)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
return content_tempfile
|
|
||||||
|
|
||||||
def _get_diff_data(self, conn, tmp, inject, destination, source):
|
|
||||||
peek_result = self.runner._execute_module(conn, tmp, 'file', "path=%s diff_peek=1" % destination, inject=inject, persist_files=True)
|
|
||||||
|
|
||||||
if not peek_result.is_successful():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
diff = {}
|
|
||||||
if peek_result.result['state'] == 'absent':
|
|
||||||
diff['before'] = ''
|
|
||||||
elif peek_result.result['appears_binary']:
|
|
||||||
diff['dst_binary'] = 1
|
|
||||||
elif peek_result.result['size'] > utils.MAX_FILE_SIZE_FOR_DIFF:
|
|
||||||
diff['dst_larger'] = utils.MAX_FILE_SIZE_FOR_DIFF
|
|
||||||
else:
|
|
||||||
dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % destination, inject=inject, persist_files=True)
|
|
||||||
if 'content' in dest_result.result:
|
|
||||||
dest_contents = dest_result.result['content']
|
|
||||||
if dest_result.result['encoding'] == 'base64':
|
|
||||||
dest_contents = base64.b64decode(dest_contents)
|
|
||||||
else:
|
|
||||||
raise Exception("unknown encoding, failed: %s" % dest_result.result)
|
|
||||||
diff['before_header'] = destination
|
|
||||||
diff['before'] = dest_contents
|
|
||||||
|
|
||||||
src = open(source)
|
|
||||||
src_contents = src.read(8192)
|
|
||||||
st = os.stat(source)
|
|
||||||
if "\x00" in src_contents:
|
|
||||||
diff['src_binary'] = 1
|
|
||||||
elif st[stat.ST_SIZE] > utils.MAX_FILE_SIZE_FOR_DIFF:
|
|
||||||
diff['src_larger'] = utils.MAX_FILE_SIZE_FOR_DIFF
|
|
||||||
else:
|
|
||||||
src.seek(0)
|
|
||||||
diff['after_header'] = source
|
|
||||||
diff['after'] = src.read()
|
|
||||||
|
|
||||||
return diff
|
|
||||||
|
|
||||||
def _remove_tempfile_if_content_defined(self, content, content_tempfile):
|
|
||||||
if content is not None:
|
|
||||||
os.remove(content_tempfile)
|
|
||||||
|
|
||||||
|
|
||||||
def _result_key_merge(self, options, results):
|
|
||||||
# add keys to file module results to mimic copy
|
|
||||||
if 'path' in results.result and 'dest' not in results.result:
|
|
||||||
results.result['dest'] = results.result['path']
|
|
||||||
del results.result['path']
|
|
||||||
return results
|
|
@ -1,60 +0,0 @@
|
|||||||
# Copyright 2012, Dag Wieers <dag@wieers.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/>.
|
|
||||||
|
|
||||||
import ansible
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.utils import template
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
''' Print statements during execution '''
|
|
||||||
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
self.basedir = runner.basedir
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
args = {}
|
|
||||||
if complex_args:
|
|
||||||
args.update(complex_args)
|
|
||||||
|
|
||||||
# attempt to prevent confusing messages when the variable didn't interpolate
|
|
||||||
module_args = module_args.replace("{{ ","{{").replace(" }}","}}")
|
|
||||||
|
|
||||||
kv = utils.parse_kv(module_args)
|
|
||||||
args.update(kv)
|
|
||||||
|
|
||||||
if not 'msg' in args and not 'var' in args:
|
|
||||||
args['msg'] = 'Hello world!'
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
if 'msg' in args:
|
|
||||||
if 'fail' in args and utils.boolean(args['fail']):
|
|
||||||
result = dict(failed=True, msg=args['msg'])
|
|
||||||
else:
|
|
||||||
result = dict(msg=args['msg'])
|
|
||||||
elif 'var' in args and not utils.LOOKUP_REGEX.search(args['var']):
|
|
||||||
results = template.template(self.basedir, args['var'], inject, convert_bare=True)
|
|
||||||
result['var'] = { args['var']: results }
|
|
||||||
|
|
||||||
# force flag to make debug output module always verbose
|
|
||||||
result['verbose_always'] = True
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
@ -1,44 +0,0 @@
|
|||||||
# Copyright 2012, Dag Wieers <dag@wieers.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/>.
|
|
||||||
|
|
||||||
import ansible
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
''' Fail with custom message '''
|
|
||||||
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
# note: the fail module does not need to pay attention to check mode
|
|
||||||
# it always runs.
|
|
||||||
|
|
||||||
args = {}
|
|
||||||
if complex_args:
|
|
||||||
args.update(complex_args)
|
|
||||||
args.update(utils.parse_kv(module_args))
|
|
||||||
if not 'msg' in args:
|
|
||||||
args['msg'] = 'Failed as requested from task'
|
|
||||||
|
|
||||||
result = dict(failed=True, msg=args['msg'])
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
@ -1,173 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pwd
|
|
||||||
import random
|
|
||||||
import traceback
|
|
||||||
import tempfile
|
|
||||||
import base64
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import module_common
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for fetch operations '''
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not (yet) supported for this module'))
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
source = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
flat = options.get('flat', False)
|
|
||||||
flat = utils.boolean(flat)
|
|
||||||
fail_on_missing = options.get('fail_on_missing', False)
|
|
||||||
fail_on_missing = utils.boolean(fail_on_missing)
|
|
||||||
validate_checksum = options.get('validate_checksum', None)
|
|
||||||
if validate_checksum is not None:
|
|
||||||
validate_checksum = utils.boolean(validate_checksum)
|
|
||||||
# Alias for validate_checksum (old way of specifying it)
|
|
||||||
validate_md5 = options.get('validate_md5', None)
|
|
||||||
if validate_md5 is not None:
|
|
||||||
validate_md5 = utils.boolean(validate_md5)
|
|
||||||
if validate_md5 is None and validate_checksum is None:
|
|
||||||
# Default
|
|
||||||
validate_checksum = True
|
|
||||||
elif validate_checksum is None:
|
|
||||||
validate_checksum = validate_md5
|
|
||||||
elif validate_md5 is not None and validate_checksum is not None:
|
|
||||||
results = dict(failed=True, msg="validate_checksum and validate_md5 cannot both be specified")
|
|
||||||
return ReturnData(conn, result=results)
|
|
||||||
|
|
||||||
if source is None or dest is None:
|
|
||||||
results = dict(failed=True, msg="src and dest are required")
|
|
||||||
return ReturnData(conn=conn, result=results)
|
|
||||||
|
|
||||||
source = conn.shell.join_path(source)
|
|
||||||
source = self.runner._remote_expand_user(conn, source, tmp)
|
|
||||||
|
|
||||||
# calculate checksum for the remote file
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp, source, inject)
|
|
||||||
|
|
||||||
# use slurp if sudo and permissions are lacking
|
|
||||||
remote_data = None
|
|
||||||
if remote_checksum in ('1', '2') or self.runner.become:
|
|
||||||
slurpres = self.runner._execute_module(conn, tmp, 'slurp', 'src=%s' % source, inject=inject)
|
|
||||||
if slurpres.is_successful():
|
|
||||||
if slurpres.result['encoding'] == 'base64':
|
|
||||||
remote_data = base64.b64decode(slurpres.result['content'])
|
|
||||||
if remote_data is not None:
|
|
||||||
remote_checksum = utils.checksum_s(remote_data)
|
|
||||||
# the source path may have been expanded on the
|
|
||||||
# target system, so we compare it here and use the
|
|
||||||
# expanded version if it's different
|
|
||||||
remote_source = slurpres.result.get('source')
|
|
||||||
if remote_source and remote_source != source:
|
|
||||||
source = remote_source
|
|
||||||
|
|
||||||
# calculate the destination name
|
|
||||||
if os.path.sep not in conn.shell.join_path('a', ''):
|
|
||||||
source_local = source.replace('\\', '/')
|
|
||||||
else:
|
|
||||||
source_local = source
|
|
||||||
|
|
||||||
dest = os.path.expanduser(dest)
|
|
||||||
if flat:
|
|
||||||
if dest.endswith("/"):
|
|
||||||
# if the path ends with "/", we'll use the source filename as the
|
|
||||||
# destination filename
|
|
||||||
base = os.path.basename(source_local)
|
|
||||||
dest = os.path.join(dest, base)
|
|
||||||
if not dest.startswith("/"):
|
|
||||||
# if dest does not start with "/", we'll assume a relative path
|
|
||||||
dest = utils.path_dwim(self.runner.basedir, dest)
|
|
||||||
else:
|
|
||||||
# files are saved in dest dir, with a subdir for each host, then the filename
|
|
||||||
dest = "%s/%s/%s" % (utils.path_dwim(self.runner.basedir, dest), inject['inventory_hostname'], source_local)
|
|
||||||
|
|
||||||
dest = dest.replace("//","/")
|
|
||||||
|
|
||||||
if remote_checksum in ('0', '1', '2', '3', '4'):
|
|
||||||
# these don't fail because you may want to transfer a log file that possibly MAY exist
|
|
||||||
# but keep going to fetch other log files
|
|
||||||
if remote_checksum == '0':
|
|
||||||
result = dict(msg="unable to calculate the checksum of the remote file", file=source, changed=False)
|
|
||||||
elif remote_checksum == '1':
|
|
||||||
if fail_on_missing:
|
|
||||||
result = dict(failed=True, msg="the remote file does not exist", file=source)
|
|
||||||
else:
|
|
||||||
result = dict(msg="the remote file does not exist, not transferring, ignored", file=source, changed=False)
|
|
||||||
elif remote_checksum == '2':
|
|
||||||
result = dict(msg="no read permission on remote file, not transferring, ignored", file=source, changed=False)
|
|
||||||
elif remote_checksum == '3':
|
|
||||||
result = dict(msg="remote file is a directory, fetch cannot work on directories", file=source, changed=False)
|
|
||||||
elif remote_checksum == '4':
|
|
||||||
result = dict(msg="python isn't present on the system. Unable to compute checksum", file=source, changed=False)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
# calculate checksum for the local file
|
|
||||||
local_checksum = utils.checksum(dest)
|
|
||||||
|
|
||||||
if remote_checksum != local_checksum:
|
|
||||||
# create the containing directories, if needed
|
|
||||||
if not os.path.isdir(os.path.dirname(dest)):
|
|
||||||
os.makedirs(os.path.dirname(dest))
|
|
||||||
|
|
||||||
# fetch the file and check for changes
|
|
||||||
if remote_data is None:
|
|
||||||
conn.fetch_file(source, dest)
|
|
||||||
else:
|
|
||||||
f = open(dest, 'w')
|
|
||||||
f.write(remote_data)
|
|
||||||
f.close()
|
|
||||||
new_checksum = utils.secure_hash(dest)
|
|
||||||
# For backwards compatibility. We'll return None on FIPS enabled
|
|
||||||
# systems
|
|
||||||
try:
|
|
||||||
new_md5 = utils.md5(dest)
|
|
||||||
except ValueError:
|
|
||||||
new_md5 = None
|
|
||||||
|
|
||||||
if validate_checksum and new_checksum != remote_checksum:
|
|
||||||
result = dict(failed=True, md5sum=new_md5, msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
result = dict(changed=True, md5sum=new_md5, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
else:
|
|
||||||
# For backwards compatibility. We'll return None on FIPS enabled
|
|
||||||
# systems
|
|
||||||
try:
|
|
||||||
local_md5 = utils.md5(dest)
|
|
||||||
except ValueError:
|
|
||||||
local_md5 = None
|
|
||||||
|
|
||||||
result = dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
|||||||
# Copyright 2012, Jeroen Hoekx <jeroen@hoekx.be>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import ansible
|
|
||||||
|
|
||||||
from ansible.callbacks import vv
|
|
||||||
from ansible.errors import AnsibleError as ae
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
from ansible.utils import parse_kv, check_conditional
|
|
||||||
import ansible.utils.template as template
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
''' Create inventory groups based on variables '''
|
|
||||||
|
|
||||||
### We need to be able to modify the inventory
|
|
||||||
BYPASS_HOST_LOOP = True
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
# the group_by module does not need to pay attention to check mode.
|
|
||||||
# it always runs.
|
|
||||||
|
|
||||||
# module_args and complex_args have already been templated for the first host.
|
|
||||||
# Use them here only to check that a key argument is provided.
|
|
||||||
args = {}
|
|
||||||
if complex_args:
|
|
||||||
args.update(complex_args)
|
|
||||||
args.update(parse_kv(module_args))
|
|
||||||
if not 'key' in args:
|
|
||||||
raise ae("'key' is a required argument.")
|
|
||||||
|
|
||||||
vv("created 'group_by' ActionModule: key=%s"%(args['key']))
|
|
||||||
|
|
||||||
inventory = self.runner.inventory
|
|
||||||
|
|
||||||
result = {'changed': False}
|
|
||||||
|
|
||||||
### find all groups
|
|
||||||
groups = {}
|
|
||||||
|
|
||||||
for host in self.runner.host_set:
|
|
||||||
data = {}
|
|
||||||
data.update(inject)
|
|
||||||
data.update(inject['hostvars'][host])
|
|
||||||
conds = self.runner.conditional
|
|
||||||
if type(conds) != list:
|
|
||||||
conds = [ conds ]
|
|
||||||
next_host = False
|
|
||||||
for cond in conds:
|
|
||||||
if not check_conditional(cond, self.runner.basedir, data, fail_on_undefined=self.runner.error_on_undefined_vars):
|
|
||||||
next_host = True
|
|
||||||
break
|
|
||||||
if next_host:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Template original module_args and complex_args from runner for each host.
|
|
||||||
host_module_args = template.template(self.runner.basedir, self.runner.module_args, data)
|
|
||||||
host_complex_args = template.template(self.runner.basedir, self.runner.complex_args, data)
|
|
||||||
host_args = {}
|
|
||||||
if host_complex_args:
|
|
||||||
host_args.update(host_complex_args)
|
|
||||||
host_args.update(parse_kv(host_module_args))
|
|
||||||
|
|
||||||
group_name = host_args['key']
|
|
||||||
group_name = group_name.replace(' ','-')
|
|
||||||
if group_name not in groups:
|
|
||||||
groups[group_name] = []
|
|
||||||
groups[group_name].append(host)
|
|
||||||
|
|
||||||
result['groups'] = groups
|
|
||||||
|
|
||||||
### add to inventory
|
|
||||||
for group, hosts in groups.items():
|
|
||||||
inv_group = inventory.get_group(group)
|
|
||||||
if not inv_group:
|
|
||||||
inv_group = ansible.inventory.Group(name=group)
|
|
||||||
inventory.add_group(inv_group)
|
|
||||||
inventory.get_group('all').add_child_group(inv_group)
|
|
||||||
inv_group.vars = inventory.get_group_variables(group, update_cached=False, vault_password=inventory._vault_password)
|
|
||||||
for host in hosts:
|
|
||||||
if host in self.runner.inventory._vars_per_host:
|
|
||||||
del self.runner.inventory._vars_per_host[host]
|
|
||||||
inv_host = inventory.get_host(host)
|
|
||||||
if not inv_host:
|
|
||||||
inv_host = ansible.inventory.Host(name=host)
|
|
||||||
if inv_group not in inv_host.get_groups():
|
|
||||||
result['changed'] = True
|
|
||||||
inv_group.add_host(inv_host)
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=result)
|
|
@ -1,56 +0,0 @@
|
|||||||
# (c) 2013-2014, Benno Joy <benno@ansible.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
from ansible.utils import template
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
if not module_args:
|
|
||||||
result = dict(failed=True, msg="No source file given")
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=result)
|
|
||||||
|
|
||||||
source = module_args
|
|
||||||
source = template.template(self.runner.basedir, source, inject)
|
|
||||||
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'vars', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
if os.path.exists(source):
|
|
||||||
data = utils.parse_yaml_from_file(source, vault_password=self.runner.vault_pass)
|
|
||||||
if data and type(data) != dict:
|
|
||||||
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % source)
|
|
||||||
elif data is None:
|
|
||||||
data = {}
|
|
||||||
result = dict(ansible_facts=data)
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=result)
|
|
||||||
else:
|
|
||||||
result = dict(failed=True, msg="Source file not found.", file=source)
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=result)
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pwd
|
|
||||||
import random
|
|
||||||
import traceback
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import module_common
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
from ansible.callbacks import vv, vvv
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' transfer & execute a module that is not 'copy' or 'template' '''
|
|
||||||
|
|
||||||
module_args = self.runner._complex_args_hack(complex_args, module_args)
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
if module_name in [ 'shell', 'command' ]:
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for %s' % module_name))
|
|
||||||
# else let the module parsing code decide, though this will only be allowed for AnsibleModuleCommon using
|
|
||||||
# python modules for now
|
|
||||||
module_args += " CHECKMODE=True"
|
|
||||||
|
|
||||||
if self.runner.no_log:
|
|
||||||
module_args += " NO_LOG=True"
|
|
||||||
|
|
||||||
# shell and command are the same module
|
|
||||||
if module_name == 'shell':
|
|
||||||
module_name = 'command'
|
|
||||||
module_args += " #USE_SHELL"
|
|
||||||
|
|
||||||
vv("REMOTE_MODULE %s %s" % (module_name, module_args), host=conn.host)
|
|
||||||
return self.runner._execute_module(conn, tmp, module_name, module_args, inject=inject, complex_args=complex_args)
|
|
||||||
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
|||||||
# (c) 2015, Brian Coca <briancoca+dev@gmail.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
|
|
||||||
|
|
||||||
import os
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
|
|
||||||
src = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
remote_src = utils.boolean(options.get('remote_src', 'no'))
|
|
||||||
|
|
||||||
if src is None:
|
|
||||||
result = dict(failed=True, msg="src is required")
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
|
|
||||||
if remote_src:
|
|
||||||
return self.runner._execute_module(conn, tmp, 'patch', module_args, inject=inject, complex_args=complex_args)
|
|
||||||
|
|
||||||
# Source is local
|
|
||||||
if '_original_file' in inject:
|
|
||||||
src = utils.path_dwim_relative(inject['_original_file'], 'files', src, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
src = utils.path_dwim(self.runner.basedir, src)
|
|
||||||
|
|
||||||
if tmp is None or "-tmp-" not in tmp:
|
|
||||||
tmp = self.runner._make_tmp_path(conn)
|
|
||||||
|
|
||||||
tmp_src = conn.shell.join_path(tmp, os.path.basename(src))
|
|
||||||
conn.put_file(src, tmp_src)
|
|
||||||
|
|
||||||
if self.runner.become and self.runner.become_user != 'root':
|
|
||||||
if not self.runner.noop_on_check(inject):
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp)
|
|
||||||
|
|
||||||
new_module_args = dict(
|
|
||||||
src=tmp_src,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
|
|
||||||
module_args = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
return self.runner._execute_module(conn, tmp, 'patch', module_args, inject=inject, complex_args=complex_args)
|
|
@ -1,139 +0,0 @@
|
|||||||
# Copyright 2012, Tim Bielawa <tbielawa@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.callbacks import vv
|
|
||||||
from ansible.errors import AnsibleError as ae
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
from ansible.utils import getch, parse_kv
|
|
||||||
import ansible.utils.template as template
|
|
||||||
from termios import tcflush, TCIFLUSH
|
|
||||||
import datetime
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
''' pauses execution for a length or time, or until input is received '''
|
|
||||||
|
|
||||||
PAUSE_TYPES = ['seconds', 'minutes', 'prompt', '']
|
|
||||||
BYPASS_HOST_LOOP = True
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
# Set defaults
|
|
||||||
self.duration_unit = 'minutes'
|
|
||||||
self.prompt = None
|
|
||||||
self.seconds = None
|
|
||||||
self.result = {'changed': False,
|
|
||||||
'rc': 0,
|
|
||||||
'stderr': '',
|
|
||||||
'stdout': '',
|
|
||||||
'start': None,
|
|
||||||
'stop': None,
|
|
||||||
'delta': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' run the pause action module '''
|
|
||||||
|
|
||||||
# note: this module does not need to pay attention to the 'check'
|
|
||||||
# flag, it always runs
|
|
||||||
|
|
||||||
hosts = ', '.join(self.runner.host_set)
|
|
||||||
args = {}
|
|
||||||
if complex_args:
|
|
||||||
args.update(complex_args)
|
|
||||||
# extra template call unneeded?
|
|
||||||
args.update(parse_kv(template.template(self.runner.basedir, module_args, inject)))
|
|
||||||
|
|
||||||
# Are 'minutes' or 'seconds' keys that exist in 'args'?
|
|
||||||
if 'minutes' in args or 'seconds' in args:
|
|
||||||
try:
|
|
||||||
if 'minutes' in args:
|
|
||||||
self.pause_type = 'minutes'
|
|
||||||
# The time() command operates in seconds so we need to
|
|
||||||
# recalculate for minutes=X values.
|
|
||||||
self.seconds = int(args['minutes']) * 60
|
|
||||||
else:
|
|
||||||
self.pause_type = 'seconds'
|
|
||||||
self.seconds = int(args['seconds'])
|
|
||||||
self.duration_unit = 'seconds'
|
|
||||||
except ValueError, e:
|
|
||||||
raise ae("non-integer value given for prompt duration:\n%s" % str(e))
|
|
||||||
# Is 'prompt' a key in 'args'?
|
|
||||||
elif 'prompt' in args:
|
|
||||||
self.pause_type = 'prompt'
|
|
||||||
self.prompt = "[%s]\n%s:\n" % (hosts, args['prompt'])
|
|
||||||
# Is 'args' empty, then this is the default prompted pause
|
|
||||||
elif len(args.keys()) == 0:
|
|
||||||
self.pause_type = 'prompt'
|
|
||||||
self.prompt = "[%s]\nPress enter to continue:\n" % hosts
|
|
||||||
# I have no idea what you're trying to do. But it's so wrong.
|
|
||||||
else:
|
|
||||||
raise ae("invalid pause type given. must be one of: %s" % \
|
|
||||||
", ".join(self.PAUSE_TYPES))
|
|
||||||
|
|
||||||
vv("created 'pause' ActionModule: pause_type=%s, duration_unit=%s, calculated_seconds=%s, prompt=%s" % \
|
|
||||||
(self.pause_type, self.duration_unit, self.seconds, self.prompt))
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
# Begin the hard work!
|
|
||||||
try:
|
|
||||||
self._start()
|
|
||||||
if not self.pause_type == 'prompt':
|
|
||||||
print "[%s]\nPausing for %s seconds" % (hosts, self.seconds)
|
|
||||||
time.sleep(self.seconds)
|
|
||||||
else:
|
|
||||||
# Clear out any unflushed buffered input which would
|
|
||||||
# otherwise be consumed by raw_input() prematurely.
|
|
||||||
tcflush(sys.stdin, TCIFLUSH)
|
|
||||||
self.result['user_input'] = raw_input(self.prompt.encode(sys.stdout.encoding))
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
while True:
|
|
||||||
print '\nAction? (a)bort/(c)ontinue: '
|
|
||||||
c = getch()
|
|
||||||
if c == 'c':
|
|
||||||
# continue playbook evaluation
|
|
||||||
break
|
|
||||||
elif c == 'a':
|
|
||||||
# abort further playbook evaluation
|
|
||||||
raise ae('user requested abort!')
|
|
||||||
finally:
|
|
||||||
self._stop()
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, result=self.result)
|
|
||||||
|
|
||||||
def _start(self):
|
|
||||||
''' mark the time of execution for duration calculations later '''
|
|
||||||
self.start = time.time()
|
|
||||||
self.result['start'] = str(datetime.datetime.now())
|
|
||||||
if not self.pause_type == 'prompt':
|
|
||||||
print "(^C-c = continue early, ^C-a = abort)"
|
|
||||||
|
|
||||||
def _stop(self):
|
|
||||||
''' calculate the duration we actually paused for and then
|
|
||||||
finish building the task result string '''
|
|
||||||
duration = time.time() - self.start
|
|
||||||
self.result['stop'] = str(datetime.datetime.now())
|
|
||||||
self.result['delta'] = int(duration)
|
|
||||||
|
|
||||||
if self.duration_unit == 'minutes':
|
|
||||||
duration = round(duration / 60.0, 2)
|
|
||||||
else:
|
|
||||||
duration = round(duration, 2)
|
|
||||||
|
|
||||||
self.result['stdout'] = "Paused for %s %s" % (duration, self.duration_unit)
|
|
@ -1,54 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
# in --check mode, always skip this module execution
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True))
|
|
||||||
|
|
||||||
executable = ''
|
|
||||||
# From library/command, keep in sync
|
|
||||||
r = re.compile(r'(^|\s)(executable)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)\s|$)')
|
|
||||||
for m in r.finditer(module_args):
|
|
||||||
v = m.group(4).replace("\\", "")
|
|
||||||
if m.group(2) == "executable":
|
|
||||||
executable = v
|
|
||||||
module_args = r.sub("", module_args)
|
|
||||||
|
|
||||||
result = self.runner._low_level_exec_command(conn, module_args, tmp, sudoable=True, executable=executable,
|
|
||||||
become=self.runner.become)
|
|
||||||
# for some modules (script, raw), the sudo success key
|
|
||||||
# may leak into the stdout due to the way the sudo/su
|
|
||||||
# command is constructed, so we filter that out here
|
|
||||||
if result.get('stdout','').strip().startswith('BECOME-SUCCESS-'):
|
|
||||||
result['stdout'] = re.sub(r'^((\r)?\n)?BECOME-SUCCESS.*(\r)?\n', '', result['stdout'])
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
@ -1,136 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible.utils import template
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
TRANSFERS_FILES = True
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for file transfer operations '''
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
# in check mode, always skip this module
|
|
||||||
return ReturnData(conn=conn, comm_ok=True,
|
|
||||||
result=dict(skipped=True, msg='check mode not supported for this module'))
|
|
||||||
|
|
||||||
# extract ansible reserved parameters
|
|
||||||
# From library/command keep in sync
|
|
||||||
creates = None
|
|
||||||
removes = None
|
|
||||||
r = re.compile(r'(^|\s)(creates|removes)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
|
|
||||||
for m in r.finditer(module_args):
|
|
||||||
v = m.group(4).replace("\\", "")
|
|
||||||
if m.group(2) == "creates":
|
|
||||||
creates = v
|
|
||||||
elif m.group(2) == "removes":
|
|
||||||
removes = v
|
|
||||||
module_args = r.sub("", module_args)
|
|
||||||
|
|
||||||
if creates:
|
|
||||||
# do not run the command if the line contains creates=filename
|
|
||||||
# and the filename already exists. This allows idempotence
|
|
||||||
# of command executions.
|
|
||||||
module_args_tmp = "path=%s" % creates
|
|
||||||
module_return = self.runner._execute_module(conn, tmp, 'stat', module_args_tmp, inject=inject,
|
|
||||||
complex_args=complex_args, persist_files=True)
|
|
||||||
stat = module_return.result.get('stat', None)
|
|
||||||
if stat and stat.get('exists', False):
|
|
||||||
return ReturnData(
|
|
||||||
conn=conn,
|
|
||||||
comm_ok=True,
|
|
||||||
result=dict(
|
|
||||||
changed=False,
|
|
||||||
msg=("skipped, since %s exists" % creates)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if removes:
|
|
||||||
# do not run the command if the line contains removes=filename
|
|
||||||
# and the filename does not exist. This allows idempotence
|
|
||||||
# of command executions.
|
|
||||||
module_args_tmp = "path=%s" % removes
|
|
||||||
module_return = self.runner._execute_module(conn, tmp, 'stat', module_args_tmp, inject=inject,
|
|
||||||
complex_args=complex_args, persist_files=True)
|
|
||||||
stat = module_return.result.get('stat', None)
|
|
||||||
if stat and not stat.get('exists', False):
|
|
||||||
return ReturnData(
|
|
||||||
conn=conn,
|
|
||||||
comm_ok=True,
|
|
||||||
result=dict(
|
|
||||||
changed=False,
|
|
||||||
msg=("skipped, since %s does not exist" % removes)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Decode the result of shlex.split() to UTF8 to get around a bug in that's been fixed in Python 2.7 but not Python 2.6.
|
|
||||||
# See: http://bugs.python.org/issue6988
|
|
||||||
tokens = shlex.split(module_args.encode('utf8'))
|
|
||||||
tokens = [s.decode('utf8') for s in tokens]
|
|
||||||
# extract source script
|
|
||||||
source = tokens[0]
|
|
||||||
|
|
||||||
# FIXME: error handling
|
|
||||||
args = " ".join(tokens[1:])
|
|
||||||
source = template.template(self.runner.basedir, source, inject)
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'files', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
# transfer the file to a remote tmp location
|
|
||||||
source = source.replace('\x00', '') # why does this happen here?
|
|
||||||
args = args.replace('\x00', '') # why does this happen here?
|
|
||||||
tmp_src = conn.shell.join_path(tmp, os.path.basename(source))
|
|
||||||
tmp_src = tmp_src.replace('\x00', '')
|
|
||||||
|
|
||||||
conn.put_file(source, tmp_src)
|
|
||||||
|
|
||||||
sudoable = True
|
|
||||||
# set file permissions, more permissive when the copy is done as a different user
|
|
||||||
if self.runner.become and self.runner.become_user != 'root':
|
|
||||||
chmod_mode = 'a+rx'
|
|
||||||
sudoable = False
|
|
||||||
else:
|
|
||||||
chmod_mode = '+rx'
|
|
||||||
self.runner._remote_chmod(conn, chmod_mode, tmp_src, tmp, sudoable=sudoable, become=self.runner.become)
|
|
||||||
|
|
||||||
# add preparation steps to one ssh roundtrip executing the script
|
|
||||||
env_string = self.runner._compute_environment_string(conn, inject)
|
|
||||||
module_args = ' '.join([env_string, tmp_src, args])
|
|
||||||
|
|
||||||
handler = utils.plugins.action_loader.get('raw', self.runner)
|
|
||||||
result = handler.run(conn, tmp, 'raw', module_args, inject)
|
|
||||||
|
|
||||||
# clean up after
|
|
||||||
if "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES:
|
|
||||||
self.runner._remove_tmp_path(conn, tmp)
|
|
||||||
|
|
||||||
result.result['changed'] = True
|
|
||||||
|
|
||||||
return result
|
|
@ -1,47 +0,0 @@
|
|||||||
# Copyright 2013 Dag Wieers <dag@wieers.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 import utils
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
TRANSFERS_FILES = False
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for running operations on master '''
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
|
|
||||||
# parse the k=v arguments and convert any special boolean
|
|
||||||
# strings into proper booleans (issue #8629)
|
|
||||||
parsed_args = utils.parse_kv(module_args)
|
|
||||||
for k,v in parsed_args.iteritems():
|
|
||||||
# convert certain strings to boolean values
|
|
||||||
if isinstance(v, basestring) and v.lower() in ('true', 'false', 'yes', 'no'):
|
|
||||||
parsed_args[k] = utils.boolean(v)
|
|
||||||
|
|
||||||
# and finally update the options with the parsed/modified args
|
|
||||||
options.update(parsed_args)
|
|
||||||
|
|
||||||
return ReturnData(conn=conn, result=dict(ansible_facts=options))
|
|
@ -1,218 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# (c) 2012-2013, Timothy Appnel <tim@appnel.com>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import constants
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
import ansible.utils.template as template
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
self.inject = None
|
|
||||||
|
|
||||||
def _get_absolute_path(self, path=None):
|
|
||||||
if 'vars' in self.inject:
|
|
||||||
if '_original_file' in self.inject['vars']:
|
|
||||||
# roles
|
|
||||||
original_path = path
|
|
||||||
path = utils.path_dwim_relative(self.inject['_original_file'], 'files', path, self.runner.basedir)
|
|
||||||
if original_path and original_path[-1] == '/' and path[-1] != '/':
|
|
||||||
# make sure the dwim'd path ends in a trailing "/"
|
|
||||||
# if the original path did
|
|
||||||
path += '/'
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
def _process_origin(self, host, path, user):
|
|
||||||
|
|
||||||
if not host in ['127.0.0.1', 'localhost']:
|
|
||||||
if user:
|
|
||||||
return '%s@%s:%s' % (user, host, path)
|
|
||||||
else:
|
|
||||||
return '%s:%s' % (host, path)
|
|
||||||
else:
|
|
||||||
if not ':' in path:
|
|
||||||
if not path.startswith('/'):
|
|
||||||
path = self._get_absolute_path(path=path)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def _process_remote(self, host, path, user):
|
|
||||||
transport = self.runner.transport
|
|
||||||
return_data = None
|
|
||||||
if not host in ['127.0.0.1', 'localhost'] or transport != "local":
|
|
||||||
if user:
|
|
||||||
return_data = '%s@%s:%s' % (user, host, path)
|
|
||||||
else:
|
|
||||||
return_data = '%s:%s' % (host, path)
|
|
||||||
else:
|
|
||||||
return_data = path
|
|
||||||
|
|
||||||
if not ':' in return_data:
|
|
||||||
if not return_data.startswith('/'):
|
|
||||||
return_data = self._get_absolute_path(path=return_data)
|
|
||||||
|
|
||||||
return return_data
|
|
||||||
|
|
||||||
def setup(self, module_name, inject):
|
|
||||||
''' Always default to localhost as delegate if None defined '''
|
|
||||||
|
|
||||||
self.inject = inject
|
|
||||||
|
|
||||||
# Store original transport and sudo values.
|
|
||||||
self.original_transport = inject.get('ansible_connection', self.runner.transport)
|
|
||||||
self.original_become = self.runner.become
|
|
||||||
self.transport_overridden = False
|
|
||||||
|
|
||||||
if inject.get('delegate_to') is None:
|
|
||||||
inject['delegate_to'] = '127.0.0.1'
|
|
||||||
# IF original transport is not local, override transport and disable sudo.
|
|
||||||
if self.original_transport != 'local':
|
|
||||||
inject['ansible_connection'] = 'local'
|
|
||||||
self.transport_overridden = True
|
|
||||||
self.runner.become = False
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args,
|
|
||||||
inject, complex_args=None, **kwargs):
|
|
||||||
|
|
||||||
''' generates params and passes them on to the rsync module '''
|
|
||||||
|
|
||||||
self.inject = inject
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
|
|
||||||
src = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
use_ssh_args = options.pop('use_ssh_args', None)
|
|
||||||
|
|
||||||
src = template.template(self.runner.basedir, src, inject)
|
|
||||||
dest = template.template(self.runner.basedir, dest, inject)
|
|
||||||
use_ssh_args = template.template(self.runner.basedir, use_ssh_args, inject)
|
|
||||||
|
|
||||||
try:
|
|
||||||
options['local_rsync_path'] = inject['ansible_rsync_path']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# from the perspective of the rsync call the delegate is the localhost
|
|
||||||
src_host = '127.0.0.1'
|
|
||||||
dest_host = inject.get('ansible_ssh_host', inject['inventory_hostname'])
|
|
||||||
|
|
||||||
# allow ansible_ssh_host to be templated
|
|
||||||
dest_host = template.template(self.runner.basedir, dest_host, inject, fail_on_undefined=True)
|
|
||||||
dest_is_local = dest_host in ['127.0.0.1', 'localhost']
|
|
||||||
|
|
||||||
# CHECK FOR NON-DEFAULT SSH PORT
|
|
||||||
dest_port = options.get('dest_port')
|
|
||||||
inv_port = inject.get('ansible_ssh_port', inject['inventory_hostname'])
|
|
||||||
if inv_port != dest_port and inv_port != inject['inventory_hostname']:
|
|
||||||
options['dest_port'] = inv_port
|
|
||||||
|
|
||||||
# edge case: explicit delegate and dest_host are the same
|
|
||||||
if dest_host == inject['delegate_to']:
|
|
||||||
dest_host = '127.0.0.1'
|
|
||||||
|
|
||||||
# SWITCH SRC AND DEST PER MODE
|
|
||||||
if options.get('mode', 'push') == 'pull':
|
|
||||||
(dest_host, src_host) = (src_host, dest_host)
|
|
||||||
|
|
||||||
# CHECK DELEGATE HOST INFO
|
|
||||||
use_delegate = False
|
|
||||||
if conn.delegate != conn.host:
|
|
||||||
if 'hostvars' in inject:
|
|
||||||
if conn.delegate in inject['hostvars'] and self.original_transport != 'local':
|
|
||||||
# use a delegate host instead of localhost
|
|
||||||
use_delegate = True
|
|
||||||
|
|
||||||
# COMPARE DELEGATE, HOST AND TRANSPORT
|
|
||||||
process_args = False
|
|
||||||
if not dest_host is src_host and self.original_transport != 'local':
|
|
||||||
# interpret and inject remote host info into src or dest
|
|
||||||
process_args = True
|
|
||||||
|
|
||||||
# MUNGE SRC AND DEST PER REMOTE_HOST INFO
|
|
||||||
if process_args or use_delegate:
|
|
||||||
|
|
||||||
user = None
|
|
||||||
if utils.boolean(options.get('set_remote_user', 'yes')):
|
|
||||||
if use_delegate:
|
|
||||||
user = inject['hostvars'][conn.delegate].get('ansible_ssh_user')
|
|
||||||
|
|
||||||
if not use_delegate or not user:
|
|
||||||
user = inject.get('ansible_ssh_user',
|
|
||||||
self.runner.remote_user)
|
|
||||||
|
|
||||||
if use_delegate:
|
|
||||||
# FIXME
|
|
||||||
private_key = inject.get('ansible_ssh_private_key_file', self.runner.private_key_file)
|
|
||||||
else:
|
|
||||||
private_key = inject.get('ansible_ssh_private_key_file', self.runner.private_key_file)
|
|
||||||
|
|
||||||
private_key = template.template(self.runner.basedir, private_key, inject, fail_on_undefined=True)
|
|
||||||
|
|
||||||
if not private_key is None:
|
|
||||||
private_key = os.path.expanduser(private_key)
|
|
||||||
options['private_key'] = private_key
|
|
||||||
|
|
||||||
# use the mode to define src and dest's url
|
|
||||||
if options.get('mode', 'push') == 'pull':
|
|
||||||
# src is a remote path: <user>@<host>, dest is a local path
|
|
||||||
src = self._process_remote(src_host, src, user)
|
|
||||||
dest = self._process_origin(dest_host, dest, user)
|
|
||||||
else:
|
|
||||||
# src is a local path, dest is a remote path: <user>@<host>
|
|
||||||
src = self._process_origin(src_host, src, user)
|
|
||||||
dest = self._process_remote(dest_host, dest, user)
|
|
||||||
|
|
||||||
options['src'] = src
|
|
||||||
options['dest'] = dest
|
|
||||||
if 'mode' in options:
|
|
||||||
del options['mode']
|
|
||||||
if use_ssh_args:
|
|
||||||
options['ssh_args'] = constants.ANSIBLE_SSH_ARGS
|
|
||||||
|
|
||||||
# Allow custom rsync path argument.
|
|
||||||
rsync_path = options.get('rsync_path', None)
|
|
||||||
|
|
||||||
# If no rsync_path is set, sudo was originally set, and dest is remote then add 'sudo rsync' argument.
|
|
||||||
if not rsync_path and self.transport_overridden and self.original_become and not dest_is_local and self.runner.become_method == 'sudo':
|
|
||||||
rsync_path = 'sudo rsync'
|
|
||||||
|
|
||||||
# make sure rsync path is quoted.
|
|
||||||
if rsync_path:
|
|
||||||
options['rsync_path'] = '"' + rsync_path + '"'
|
|
||||||
|
|
||||||
module_args = ""
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
module_args = "CHECKMODE=True"
|
|
||||||
|
|
||||||
# run the module and store the result
|
|
||||||
result = self.runner._execute_module(conn, tmp, 'synchronize', module_args, complex_args=options, inject=inject)
|
|
||||||
|
|
||||||
# reset the sudo property
|
|
||||||
self.runner.become = self.original_become
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pipes
|
|
||||||
from ansible.utils import template
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
import base64
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
TRANSFERS_FILES = True
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def get_checksum(self, conn, tmp, dest, inject, try_directory=False, source=None):
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp, dest, inject)
|
|
||||||
|
|
||||||
if remote_checksum in ('0', '2', '3', '4'):
|
|
||||||
# Note: 1 means the file is not present which is fine; template
|
|
||||||
# will create it. 3 means directory was specified instead of file
|
|
||||||
# which requires special handling
|
|
||||||
if try_directory and remote_checksum == '3' and source:
|
|
||||||
# If the user specified a directory name as their dest then we
|
|
||||||
# have to check the checksum of dest/basename(src). This is
|
|
||||||
# the same behaviour as cp foo.txt /var/tmp/ so users expect
|
|
||||||
# it to work.
|
|
||||||
base = os.path.basename(source)
|
|
||||||
dest = os.path.join(dest, base)
|
|
||||||
remote_checksum = self.get_checksum(conn, tmp, dest, inject, try_directory=False)
|
|
||||||
if remote_checksum not in ('0', '2', '3', '4'):
|
|
||||||
return remote_checksum
|
|
||||||
|
|
||||||
result = dict(failed=True, msg="failed to checksum remote file."
|
|
||||||
" Checksum error code: %s" % remote_checksum)
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=result)
|
|
||||||
|
|
||||||
return remote_checksum
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for template operations '''
|
|
||||||
|
|
||||||
if not self.runner.is_playbook:
|
|
||||||
raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks")
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
|
|
||||||
source = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
|
|
||||||
if (source is None and 'first_available_file' not in inject) or dest is None:
|
|
||||||
result = dict(failed=True, msg="src and dest are required")
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
|
|
||||||
# if we have first_available_file in our vars
|
|
||||||
# look up the files and use the first one we find as src
|
|
||||||
|
|
||||||
if 'first_available_file' in inject:
|
|
||||||
found = False
|
|
||||||
for fn in self.runner.module_vars.get('first_available_file'):
|
|
||||||
fn_orig = fn
|
|
||||||
fnt = template.template(self.runner.basedir, fn, inject)
|
|
||||||
fnd = utils.path_dwim(self.runner.basedir, fnt)
|
|
||||||
if not os.path.exists(fnd) and '_original_file' in inject:
|
|
||||||
fnd = utils.path_dwim_relative(inject['_original_file'], 'templates', fnt, self.runner.basedir, check=False)
|
|
||||||
if os.path.exists(fnd):
|
|
||||||
source = fnd
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
result = dict(failed=True, msg="could not find src in first_available_file list")
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
else:
|
|
||||||
source = template.template(self.runner.basedir, source, inject)
|
|
||||||
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'templates', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
# template the source data locally & get ready to transfer
|
|
||||||
try:
|
|
||||||
resultant = template.template_from_file(self.runner.basedir, source, inject, vault_password=self.runner.vault_pass)
|
|
||||||
except Exception, e:
|
|
||||||
result = dict(failed=True, msg=type(e).__name__ + ": " + str(e))
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
|
|
||||||
# Expand any user home dir specification
|
|
||||||
dest = self.runner._remote_expand_user(conn, dest, tmp)
|
|
||||||
|
|
||||||
directory_prepended = False
|
|
||||||
if dest.endswith("/"): # CCTODO: Fix path for Windows hosts.
|
|
||||||
directory_prepended = True
|
|
||||||
base = os.path.basename(source)
|
|
||||||
dest = os.path.join(dest, base)
|
|
||||||
|
|
||||||
local_checksum = utils.checksum_s(resultant)
|
|
||||||
remote_checksum = self.get_checksum(conn, tmp, dest, inject, not directory_prepended, source=source)
|
|
||||||
|
|
||||||
if local_checksum != remote_checksum:
|
|
||||||
|
|
||||||
# template is different from the remote value
|
|
||||||
|
|
||||||
# if showing diffs, we need to get the remote value
|
|
||||||
dest_contents = ''
|
|
||||||
|
|
||||||
if self.runner.diff:
|
|
||||||
# using persist_files to keep the temp directory around to avoid needing to grab another
|
|
||||||
dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % dest, inject=inject, persist_files=True)
|
|
||||||
if 'content' in dest_result.result:
|
|
||||||
dest_contents = dest_result.result['content']
|
|
||||||
if dest_result.result['encoding'] == 'base64':
|
|
||||||
dest_contents = base64.b64decode(dest_contents)
|
|
||||||
else:
|
|
||||||
raise Exception("unknown encoding, failed: %s" % dest_result.result)
|
|
||||||
|
|
||||||
xfered = self.runner._transfer_str(conn, tmp, 'source', resultant)
|
|
||||||
|
|
||||||
# fix file permissions when the copy is done as a different user
|
|
||||||
if self.runner.become and self.runner.become_user != 'root':
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
|
|
||||||
|
|
||||||
# run the copy module
|
|
||||||
new_module_args = dict(
|
|
||||||
src=xfered,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=os.path.basename(source),
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=source, before=dest_contents, after=resultant))
|
|
||||||
else:
|
|
||||||
res = self.runner._execute_module(conn, tmp, 'copy', module_args_tmp, inject=inject, complex_args=complex_args)
|
|
||||||
if res.result.get('changed', False):
|
|
||||||
res.diff = dict(before=dest_contents, after=resultant)
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
# when running the file module based on the template data, we do
|
|
||||||
# not want the source filename (the name of the template) to be used,
|
|
||||||
# since this would mess up links, so we clear the src param and tell
|
|
||||||
# the module to follow links. When doing that, we have to set
|
|
||||||
# original_basename to the template just in case the dest is
|
|
||||||
# a directory.
|
|
||||||
module_args = ''
|
|
||||||
new_module_args = dict(
|
|
||||||
src=None,
|
|
||||||
original_basename=os.path.basename(source),
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
# be sure to inject the check mode param into the module args and
|
|
||||||
# rely on the file module to report its changed status
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
options.update(new_module_args)
|
|
||||||
return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject, complex_args=options)
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# (c) 2013, Dylan Martin <dmartin@seattlecentral.edu>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
import ansible.utils.template as template
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
|
|
||||||
## fixes https://github.com/ansible/ansible/issues/3518
|
|
||||||
# http://mypy.pythonblogs.com/12_mypy/archive/1253_workaround_for_python_bug_ascii_codec_cant_encode_character_uxa0_in_position_111_ordinal_not_in_range128.html
|
|
||||||
import sys
|
|
||||||
reload(sys)
|
|
||||||
sys.setdefaultencoding("utf8")
|
|
||||||
import pipes
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
TRANSFERS_FILES = True
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for file transfer operations '''
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
source = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
copy = utils.boolean(options.get('copy', 'yes'))
|
|
||||||
creates = options.get('creates', None)
|
|
||||||
|
|
||||||
if source is None or dest is None:
|
|
||||||
result = dict(failed=True, msg="src (or content) and dest are required")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
if creates:
|
|
||||||
# do not run the command if the line contains creates=filename
|
|
||||||
# and the filename already exists. This allows idempotence
|
|
||||||
# of command executions.
|
|
||||||
module_args_tmp = ""
|
|
||||||
complex_args_tmp = dict(path=creates, get_md5=False, get_checksum=False)
|
|
||||||
module_return = self.runner._execute_module(conn, tmp, 'stat', module_args_tmp, inject=inject,
|
|
||||||
complex_args=complex_args_tmp, delete_remote_tmp=False)
|
|
||||||
stat = module_return.result.get('stat', None)
|
|
||||||
if stat and stat.get('exists', False):
|
|
||||||
return ReturnData(
|
|
||||||
conn=conn,
|
|
||||||
comm_ok=True,
|
|
||||||
result=dict(
|
|
||||||
changed=False,
|
|
||||||
msg=("skipped, since %s exists" % creates)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = self.runner._remote_expand_user(conn, dest, tmp) # CCTODO: Fix path for Windows hosts.
|
|
||||||
source = template.template(self.runner.basedir, os.path.expanduser(source), inject)
|
|
||||||
if copy:
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'files', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp, dest, inject)
|
|
||||||
if remote_checksum == '4':
|
|
||||||
result = dict(failed=True, msg="python isn't present on the system. Unable to compute checksum")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
if remote_checksum != '3':
|
|
||||||
result = dict(failed=True, msg="dest '%s' must be an existing dir" % dest)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
if copy:
|
|
||||||
# transfer the file to a remote tmp location
|
|
||||||
tmp_src = tmp + 'source'
|
|
||||||
conn.put_file(source, tmp_src)
|
|
||||||
|
|
||||||
# handle diff mode client side
|
|
||||||
# handle check mode client side
|
|
||||||
# fix file permissions when the copy is done as a different user
|
|
||||||
if copy:
|
|
||||||
if self.runner.become and self.runner.become_user != 'root':
|
|
||||||
if not self.runner.noop_on_check(inject):
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp)
|
|
||||||
# Build temporary module_args.
|
|
||||||
new_module_args = dict(
|
|
||||||
src=tmp_src,
|
|
||||||
original_basename=os.path.basename(source),
|
|
||||||
)
|
|
||||||
|
|
||||||
# make sure checkmod is passed on correctly
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
|
|
||||||
module_args = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
else:
|
|
||||||
module_args = "%s original_basename=%s" % (module_args, pipes.quote(os.path.basename(source)))
|
|
||||||
# make sure checkmod is passed on correctly
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
module_args += " CHECKMODE=True"
|
|
||||||
return self.runner._execute_module(conn, tmp, 'unarchive', module_args, inject=inject, complex_args=complex_args)
|
|
@ -1,377 +0,0 @@
|
|||||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
import ansible.constants as C
|
|
||||||
import ansible.utils.template as template
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import stat
|
|
||||||
import tempfile
|
|
||||||
import pipes
|
|
||||||
|
|
||||||
## fixes https://github.com/ansible/ansible/issues/3518
|
|
||||||
# http://mypy.pythonblogs.com/12_mypy/archive/1253_workaround_for_python_bug_ascii_codec_cant_encode_character_uxa0_in_position_111_ordinal_not_in_range128.html
|
|
||||||
import sys
|
|
||||||
reload(sys)
|
|
||||||
sys.setdefaultencoding("utf8")
|
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp_path, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for file transfer operations '''
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
source = options.get('src', None)
|
|
||||||
content = options.get('content', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
raw = utils.boolean(options.get('raw', 'no'))
|
|
||||||
force = utils.boolean(options.get('force', 'yes'))
|
|
||||||
|
|
||||||
# content with newlines is going to be escaped to safely load in yaml
|
|
||||||
# now we need to unescape it so that the newlines are evaluated properly
|
|
||||||
# when writing the file to disk
|
|
||||||
if content:
|
|
||||||
if isinstance(content, unicode):
|
|
||||||
try:
|
|
||||||
content = content.decode('unicode-escape')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if (source is None and content is None and not 'first_available_file' in inject) or dest is None:
|
|
||||||
result=dict(failed=True, msg="src (or content) and dest are required")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
elif (source is not None or 'first_available_file' in inject) and content is not None:
|
|
||||||
result=dict(failed=True, msg="src and content are mutually exclusive")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
# Check if the source ends with a "/"
|
|
||||||
source_trailing_slash = False
|
|
||||||
if source:
|
|
||||||
source_trailing_slash = source.endswith("/")
|
|
||||||
|
|
||||||
# Define content_tempfile in case we set it after finding content populated.
|
|
||||||
content_tempfile = None
|
|
||||||
|
|
||||||
# If content is defined make a temp file and write the content into it.
|
|
||||||
if content is not None:
|
|
||||||
try:
|
|
||||||
# If content comes to us as a dict it should be decoded json.
|
|
||||||
# We need to encode it back into a string to write it out.
|
|
||||||
if type(content) is dict:
|
|
||||||
content_tempfile = self._create_content_tempfile(json.dumps(content))
|
|
||||||
else:
|
|
||||||
content_tempfile = self._create_content_tempfile(content)
|
|
||||||
source = content_tempfile
|
|
||||||
except Exception, err:
|
|
||||||
result = dict(failed=True, msg="could not write content temp file: %s" % err)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
# if we have first_available_file in our vars
|
|
||||||
# look up the files and use the first one we find as src
|
|
||||||
elif 'first_available_file' in inject:
|
|
||||||
found = False
|
|
||||||
for fn in inject.get('first_available_file'):
|
|
||||||
fn_orig = fn
|
|
||||||
fnt = template.template(self.runner.basedir, fn, inject)
|
|
||||||
fnd = utils.path_dwim(self.runner.basedir, fnt)
|
|
||||||
if not os.path.exists(fnd) and '_original_file' in inject:
|
|
||||||
fnd = utils.path_dwim_relative(inject['_original_file'], 'files', fnt, self.runner.basedir, check=False)
|
|
||||||
if os.path.exists(fnd):
|
|
||||||
source = fnd
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
results = dict(failed=True, msg="could not find src in first_available_file list")
|
|
||||||
return ReturnData(conn=conn, result=results)
|
|
||||||
else:
|
|
||||||
source = template.template(self.runner.basedir, source, inject)
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'files', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
# A list of source file tuples (full_path, relative_path) which will try to copy to the destination
|
|
||||||
source_files = []
|
|
||||||
|
|
||||||
# If source is a directory populate our list else source is a file and translate it to a tuple.
|
|
||||||
if os.path.isdir(source):
|
|
||||||
# Get the amount of spaces to remove to get the relative path.
|
|
||||||
if source_trailing_slash:
|
|
||||||
sz = len(source) + 1
|
|
||||||
else:
|
|
||||||
sz = len(source.rsplit('/', 1)[0]) + 1
|
|
||||||
|
|
||||||
# Walk the directory and append the file tuples to source_files.
|
|
||||||
for base_path, sub_folders, files in os.walk(source):
|
|
||||||
for file in files:
|
|
||||||
full_path = os.path.join(base_path, file)
|
|
||||||
rel_path = full_path[sz:]
|
|
||||||
source_files.append((full_path, rel_path))
|
|
||||||
|
|
||||||
# If it's recursive copy, destination is always a dir,
|
|
||||||
# explicitly mark it so (note - copy module relies on this).
|
|
||||||
if not conn.shell.path_has_trailing_slash(dest):
|
|
||||||
dest = conn.shell.join_path(dest, '')
|
|
||||||
else:
|
|
||||||
source_files.append((source, os.path.basename(source)))
|
|
||||||
|
|
||||||
changed = False
|
|
||||||
diffs = []
|
|
||||||
module_result = {"changed": False}
|
|
||||||
|
|
||||||
# A register for if we executed a module.
|
|
||||||
# Used to cut down on command calls when not recursive.
|
|
||||||
module_executed = False
|
|
||||||
|
|
||||||
# Tell _execute_module to delete the file if there is one file.
|
|
||||||
delete_remote_tmp = (len(source_files) == 1)
|
|
||||||
|
|
||||||
# If this is a recursive action create a tmp_path that we can share as the _exec_module create is too late.
|
|
||||||
if not delete_remote_tmp:
|
|
||||||
if "-tmp-" not in tmp_path:
|
|
||||||
tmp_path = self.runner._make_tmp_path(conn)
|
|
||||||
|
|
||||||
# expand any user home dir specifier
|
|
||||||
dest = self.runner._remote_expand_user(conn, dest, tmp_path)
|
|
||||||
|
|
||||||
for source_full, source_rel in source_files:
|
|
||||||
# Generate a hash of the local file.
|
|
||||||
local_checksum = utils.checksum(source_full)
|
|
||||||
|
|
||||||
# If local_checksum is not defined we can't find the file so we should fail out.
|
|
||||||
if local_checksum is None:
|
|
||||||
result = dict(failed=True, msg="could not find src=%s" % source_full)
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
# This is kind of optimization - if user told us destination is
|
|
||||||
# dir, do path manipulation right away, otherwise we still check
|
|
||||||
# for dest being a dir via remote call below.
|
|
||||||
if conn.shell.path_has_trailing_slash(dest):
|
|
||||||
dest_file = conn.shell.join_path(dest, source_rel)
|
|
||||||
else:
|
|
||||||
dest_file = conn.shell.join_path(dest)
|
|
||||||
|
|
||||||
# Attempt to get the remote checksum
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp_path, dest_file, inject)
|
|
||||||
|
|
||||||
if remote_checksum == '3':
|
|
||||||
# The remote_checksum was executed on a directory.
|
|
||||||
if content is not None:
|
|
||||||
# If source was defined as content remove the temporary file and fail out.
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
result = dict(failed=True, msg="can not use content with a dir as dest")
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
else:
|
|
||||||
# Append the relative source location to the destination and retry remote_checksum.
|
|
||||||
dest_file = conn.shell.join_path(dest, source_rel)
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp_path, dest_file, inject)
|
|
||||||
|
|
||||||
if remote_checksum != '1' and not force:
|
|
||||||
# remote_file does not exist so continue to next iteration.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if local_checksum != remote_checksum:
|
|
||||||
# The checksums don't match and we will change or error out.
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# Create a tmp_path if missing only if this is not recursive.
|
|
||||||
# If this is recursive we already have a tmp_path.
|
|
||||||
if delete_remote_tmp:
|
|
||||||
if "-tmp-" not in tmp_path:
|
|
||||||
tmp_path = self.runner._make_tmp_path(conn)
|
|
||||||
|
|
||||||
if self.runner.diff and not raw:
|
|
||||||
diff = self._get_diff_data(conn, tmp_path, inject, dest_file, source_full)
|
|
||||||
else:
|
|
||||||
diff = {}
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
diffs.append(diff)
|
|
||||||
changed = True
|
|
||||||
module_result = dict(changed=True)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Define a remote directory that we will copy the file to.
|
|
||||||
tmp_src = tmp_path + 'source'
|
|
||||||
|
|
||||||
if not raw:
|
|
||||||
conn.put_file(source_full, tmp_src)
|
|
||||||
else:
|
|
||||||
conn.put_file(source_full, dest_file)
|
|
||||||
|
|
||||||
# We have copied the file remotely and no longer require our content_tempfile
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
|
|
||||||
# fix file permissions when the copy is done as a different user
|
|
||||||
if self.runner.become and self.runner.become_user != 'root' and not raw:
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', tmp_src, tmp_path)
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
# Continue to next iteration if raw is defined.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Run the copy module
|
|
||||||
|
|
||||||
# src and dest here come after original and override them
|
|
||||||
# we pass dest only to make sure it includes trailing slash in case of recursive copy
|
|
||||||
new_module_args = dict(
|
|
||||||
src=tmp_src,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=source_rel
|
|
||||||
)
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
if self.runner.no_log:
|
|
||||||
new_module_args['NO_LOG'] = True
|
|
||||||
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
module_return = self.runner._execute_module(conn, tmp_path, 'win_copy', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp)
|
|
||||||
module_executed = True
|
|
||||||
|
|
||||||
else:
|
|
||||||
# no need to transfer the file, already correct md5, but still need to call
|
|
||||||
# the file module in case we want to change attributes
|
|
||||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
# Continue to next iteration if raw is defined.
|
|
||||||
# self.runner._remove_tmp_path(conn, tmp_path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
tmp_src = tmp_path + source_rel
|
|
||||||
|
|
||||||
# Build temporary module_args.
|
|
||||||
new_module_args = dict(
|
|
||||||
src=tmp_src,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=source_rel
|
|
||||||
)
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
if self.runner.no_log:
|
|
||||||
new_module_args['NO_LOG'] = True
|
|
||||||
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
# Execute the file module.
|
|
||||||
module_return = self.runner._execute_module(conn, tmp_path, 'win_file', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp)
|
|
||||||
module_executed = True
|
|
||||||
|
|
||||||
module_result = module_return.result
|
|
||||||
if not module_result.get('checksum'):
|
|
||||||
module_result['checksum'] = local_checksum
|
|
||||||
if module_result.get('failed') == True:
|
|
||||||
return module_return
|
|
||||||
if module_result.get('changed') == True:
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# Delete tmp_path if we were recursive or if we did not execute a module.
|
|
||||||
if (not C.DEFAULT_KEEP_REMOTE_FILES and not delete_remote_tmp) \
|
|
||||||
or (not C.DEFAULT_KEEP_REMOTE_FILES and delete_remote_tmp and not module_executed):
|
|
||||||
self.runner._remove_tmp_path(conn, tmp_path)
|
|
||||||
|
|
||||||
# the file module returns the file path as 'path', but
|
|
||||||
# the copy module uses 'dest', so add it if it's not there
|
|
||||||
if 'path' in module_result and 'dest' not in module_result:
|
|
||||||
module_result['dest'] = module_result['path']
|
|
||||||
|
|
||||||
# TODO: Support detailed status/diff for multiple files
|
|
||||||
if len(source_files) == 1:
|
|
||||||
result = module_result
|
|
||||||
else:
|
|
||||||
result = dict(dest=dest, src=source, changed=changed)
|
|
||||||
if len(diffs) == 1:
|
|
||||||
return ReturnData(conn=conn, result=result, diff=diffs[0])
|
|
||||||
else:
|
|
||||||
return ReturnData(conn=conn, result=result)
|
|
||||||
|
|
||||||
def _create_content_tempfile(self, content):
|
|
||||||
''' Create a tempfile containing defined content '''
|
|
||||||
fd, content_tempfile = tempfile.mkstemp()
|
|
||||||
f = os.fdopen(fd, 'w')
|
|
||||||
try:
|
|
||||||
f.write(content)
|
|
||||||
except Exception, err:
|
|
||||||
os.remove(content_tempfile)
|
|
||||||
raise Exception(err)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
return content_tempfile
|
|
||||||
|
|
||||||
def _get_diff_data(self, conn, tmp, inject, destination, source):
|
|
||||||
peek_result = self.runner._execute_module(conn, tmp, 'win_file', "path=%s diff_peek=1" % destination, inject=inject, persist_files=True)
|
|
||||||
|
|
||||||
if not peek_result.is_successful():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
diff = {}
|
|
||||||
if peek_result.result['state'] == 'absent':
|
|
||||||
diff['before'] = ''
|
|
||||||
elif peek_result.result['appears_binary']:
|
|
||||||
diff['dst_binary'] = 1
|
|
||||||
elif peek_result.result['size'] > utils.MAX_FILE_SIZE_FOR_DIFF:
|
|
||||||
diff['dst_larger'] = utils.MAX_FILE_SIZE_FOR_DIFF
|
|
||||||
else:
|
|
||||||
dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % destination, inject=inject, persist_files=True)
|
|
||||||
if 'content' in dest_result.result:
|
|
||||||
dest_contents = dest_result.result['content']
|
|
||||||
if dest_result.result['encoding'] == 'base64':
|
|
||||||
dest_contents = base64.b64decode(dest_contents)
|
|
||||||
else:
|
|
||||||
raise Exception("unknown encoding, failed: %s" % dest_result.result)
|
|
||||||
diff['before_header'] = destination
|
|
||||||
diff['before'] = dest_contents
|
|
||||||
|
|
||||||
src = open(source)
|
|
||||||
src_contents = src.read(8192)
|
|
||||||
st = os.stat(source)
|
|
||||||
if "\x00" in src_contents:
|
|
||||||
diff['src_binary'] = 1
|
|
||||||
elif st[stat.ST_SIZE] > utils.MAX_FILE_SIZE_FOR_DIFF:
|
|
||||||
diff['src_larger'] = utils.MAX_FILE_SIZE_FOR_DIFF
|
|
||||||
else:
|
|
||||||
src.seek(0)
|
|
||||||
diff['after_header'] = source
|
|
||||||
diff['after'] = src.read()
|
|
||||||
|
|
||||||
return diff
|
|
||||||
|
|
||||||
def _remove_tempfile_if_content_defined(self, content, content_tempfile):
|
|
||||||
if content is not None:
|
|
||||||
os.remove(content_tempfile)
|
|
||||||
|
|
||||||
|
|
||||||
def _result_key_merge(self, options, results):
|
|
||||||
# add keys to file module results to mimic copy
|
|
||||||
if 'path' in results.result and 'dest' not in results.result:
|
|
||||||
results.result['dest'] = results.result['path']
|
|
||||||
del results.result['path']
|
|
||||||
return results
|
|
@ -1,146 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pipes
|
|
||||||
from ansible.utils import template
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.runner.return_data import ReturnData
|
|
||||||
import base64
|
|
||||||
|
|
||||||
class ActionModule(object):
|
|
||||||
|
|
||||||
TRANSFERS_FILES = True
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
|
||||||
''' handler for template operations '''
|
|
||||||
|
|
||||||
if not self.runner.is_playbook:
|
|
||||||
raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks")
|
|
||||||
|
|
||||||
# load up options
|
|
||||||
options = {}
|
|
||||||
if complex_args:
|
|
||||||
options.update(complex_args)
|
|
||||||
options.update(utils.parse_kv(module_args))
|
|
||||||
|
|
||||||
source = options.get('src', None)
|
|
||||||
dest = options.get('dest', None)
|
|
||||||
|
|
||||||
if (source is None and 'first_available_file' not in inject) or dest is None:
|
|
||||||
result = dict(failed=True, msg="src and dest are required")
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
|
|
||||||
# if we have first_available_file in our vars
|
|
||||||
# look up the files and use the first one we find as src
|
|
||||||
|
|
||||||
if 'first_available_file' in inject:
|
|
||||||
found = False
|
|
||||||
for fn in self.runner.module_vars.get('first_available_file'):
|
|
||||||
fn_orig = fn
|
|
||||||
fnt = template.template(self.runner.basedir, fn, inject)
|
|
||||||
fnd = utils.path_dwim(self.runner.basedir, fnt)
|
|
||||||
if not os.path.exists(fnd) and '_original_file' in inject:
|
|
||||||
fnd = utils.path_dwim_relative(inject['_original_file'], 'templates', fnt, self.runner.basedir, check=False)
|
|
||||||
if os.path.exists(fnd):
|
|
||||||
source = fnd
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
result = dict(failed=True, msg="could not find src in first_available_file list")
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
else:
|
|
||||||
source = template.template(self.runner.basedir, source, inject)
|
|
||||||
|
|
||||||
if '_original_file' in inject:
|
|
||||||
source = utils.path_dwim_relative(inject['_original_file'], 'templates', source, self.runner.basedir)
|
|
||||||
else:
|
|
||||||
source = utils.path_dwim(self.runner.basedir, source)
|
|
||||||
|
|
||||||
if conn.shell.path_has_trailing_slash(dest):
|
|
||||||
base = os.path.basename(source)
|
|
||||||
dest = conn.shell.join_path(dest, base)
|
|
||||||
|
|
||||||
# template the source data locally & get ready to transfer
|
|
||||||
try:
|
|
||||||
resultant = template.template_from_file(self.runner.basedir, source, inject, vault_password=self.runner.vault_pass)
|
|
||||||
except Exception, e:
|
|
||||||
result = dict(failed=True, msg=type(e).__name__ + ": " + str(e))
|
|
||||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
|
||||||
|
|
||||||
local_checksum = utils.checksum_s(resultant)
|
|
||||||
remote_checksum = self.runner._remote_checksum(conn, tmp, dest, inject)
|
|
||||||
|
|
||||||
if local_checksum != remote_checksum:
|
|
||||||
|
|
||||||
# template is different from the remote value
|
|
||||||
|
|
||||||
# if showing diffs, we need to get the remote value
|
|
||||||
dest_contents = ''
|
|
||||||
|
|
||||||
if self.runner.diff:
|
|
||||||
# using persist_files to keep the temp directory around to avoid needing to grab another
|
|
||||||
dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % dest, inject=inject, persist_files=True)
|
|
||||||
if 'content' in dest_result.result:
|
|
||||||
dest_contents = dest_result.result['content']
|
|
||||||
if dest_result.result['encoding'] == 'base64':
|
|
||||||
dest_contents = base64.b64decode(dest_contents)
|
|
||||||
else:
|
|
||||||
raise Exception("unknown encoding, failed: %s" % dest_result.result)
|
|
||||||
|
|
||||||
xfered = self.runner._transfer_str(conn, tmp, 'source', resultant)
|
|
||||||
|
|
||||||
# fix file permissions when the copy is done as a different user
|
|
||||||
if self.runner.become and self.runner.become_user != 'root':
|
|
||||||
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
|
|
||||||
|
|
||||||
# run the copy module
|
|
||||||
new_module_args = dict(
|
|
||||||
src=xfered,
|
|
||||||
dest=dest,
|
|
||||||
original_basename=os.path.basename(source),
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=source, before=dest_contents, after=resultant))
|
|
||||||
else:
|
|
||||||
res = self.runner._execute_module(conn, tmp, 'win_copy', module_args_tmp, inject=inject, complex_args=complex_args)
|
|
||||||
if res.result.get('changed', False):
|
|
||||||
res.diff = dict(before=dest_contents, after=resultant)
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
# when running the file module based on the template data, we do
|
|
||||||
# not want the source filename (the name of the template) to be used,
|
|
||||||
# since this would mess up links, so we clear the src param and tell
|
|
||||||
# the module to follow links
|
|
||||||
new_module_args = dict(
|
|
||||||
src=None,
|
|
||||||
follow=True,
|
|
||||||
)
|
|
||||||
# be sure to inject the check mode param into the module args and
|
|
||||||
# rely on the file module to report its changed status
|
|
||||||
if self.runner.noop_on_check(inject):
|
|
||||||
new_module_args['CHECKMODE'] = True
|
|
||||||
module_args = utils.merge_module_args(module_args, new_module_args)
|
|
||||||
return self.runner._execute_module(conn, tmp, 'win_file', module_args, inject=inject, complex_args=complex_args)
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
|||||||
# (c) 2012-2013, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
################################################
|
|
||||||
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
import errno
|
|
||||||
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.errors import AnsibleError
|
|
||||||
|
|
||||||
class Connector(object):
|
|
||||||
''' Handles abstract connections to remote hosts '''
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def connect(self, host, port, user, password, transport, private_key_file, delegate_host):
|
|
||||||
conn = utils.plugins.connection_loader.get(transport, self.runner, host, port, user=user, password=password, private_key_file=private_key_file)
|
|
||||||
if conn is None:
|
|
||||||
raise AnsibleError("unsupported connection type: %s" % transport)
|
|
||||||
conn.delegate = delegate_host
|
|
||||||
if private_key_file:
|
|
||||||
# If private key is readable by user other than owner, flag an error
|
|
||||||
st = None
|
|
||||||
try:
|
|
||||||
st = os.stat(private_key_file)
|
|
||||||
except (IOError, OSError), e:
|
|
||||||
if e.errno != errno.ENOENT: # file is missing, might be agent
|
|
||||||
raise(e)
|
|
||||||
|
|
||||||
if st is not None and st.st_mode & (stat.S_IRGRP | stat.S_IROTH):
|
|
||||||
raise AnsibleError("private_key_file (%s) is group-readable or world-readable and thus insecure - "
|
|
||||||
"you will probably get an SSH failure"
|
|
||||||
% (private_key_file,))
|
|
||||||
self.active = conn.connect()
|
|
||||||
return self.active
|
|
@ -1,372 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
import time
|
|
||||||
from ansible.callbacks import vvv, vvvv
|
|
||||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
|
||||||
from ansible.runner.connection_plugins.ssh import Connection as SSHConnection
|
|
||||||
from ansible.runner.connection_plugins.paramiko_ssh import Connection as ParamikoConnection
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import constants
|
|
||||||
|
|
||||||
# the chunk size to read and send, assuming mtu 1500 and
|
|
||||||
# leaving room for base64 (+33%) encoding and header (8 bytes)
|
|
||||||
# ((1400-8)/4)*3) = 1044
|
|
||||||
# which leaves room for the TCP/IP header. We set this to a
|
|
||||||
# multiple of the value to speed up file reads.
|
|
||||||
CHUNK_SIZE=1044*20
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' raw socket accelerated connection '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
|
|
||||||
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.context = None
|
|
||||||
self.conn = None
|
|
||||||
self.user = user
|
|
||||||
self.key = utils.key_for_hostname(host)
|
|
||||||
self.port = port[0]
|
|
||||||
self.accport = port[1]
|
|
||||||
self.is_connected = False
|
|
||||||
self.has_pipelining = False
|
|
||||||
self.become_methods_supported=['sudo']
|
|
||||||
|
|
||||||
if not self.port:
|
|
||||||
self.port = constants.DEFAULT_REMOTE_PORT
|
|
||||||
elif not isinstance(self.port, int):
|
|
||||||
self.port = int(self.port)
|
|
||||||
|
|
||||||
if not self.accport:
|
|
||||||
self.accport = constants.ACCELERATE_PORT
|
|
||||||
elif not isinstance(self.accport, int):
|
|
||||||
self.accport = int(self.accport)
|
|
||||||
|
|
||||||
if self.runner.original_transport == "paramiko":
|
|
||||||
self.ssh = ParamikoConnection(
|
|
||||||
runner=self.runner,
|
|
||||||
host=self.host,
|
|
||||||
port=self.port,
|
|
||||||
user=self.user,
|
|
||||||
password=password,
|
|
||||||
private_key_file=private_key_file
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.ssh = SSHConnection(
|
|
||||||
runner=self.runner,
|
|
||||||
host=self.host,
|
|
||||||
port=self.port,
|
|
||||||
user=self.user,
|
|
||||||
password=password,
|
|
||||||
private_key_file=private_key_file
|
|
||||||
)
|
|
||||||
|
|
||||||
if not getattr(self.ssh, 'shell', None):
|
|
||||||
self.ssh.shell = utils.plugins.shell_loader.get('sh')
|
|
||||||
|
|
||||||
# attempt to work around shared-memory funness
|
|
||||||
if getattr(self.runner, 'aes_keys', None):
|
|
||||||
utils.AES_KEYS = self.runner.aes_keys
|
|
||||||
|
|
||||||
def _execute_accelerate_module(self):
|
|
||||||
args = "password=%s port=%s minutes=%d debug=%d ipv6=%s" % (
|
|
||||||
base64.b64encode(self.key.__str__()),
|
|
||||||
str(self.accport),
|
|
||||||
constants.ACCELERATE_DAEMON_TIMEOUT,
|
|
||||||
int(utils.VERBOSITY),
|
|
||||||
self.runner.accelerate_ipv6,
|
|
||||||
)
|
|
||||||
if constants.ACCELERATE_MULTI_KEY:
|
|
||||||
args += " multi_key=yes"
|
|
||||||
inject = dict(password=self.key)
|
|
||||||
if getattr(self.runner, 'accelerate_inventory_host', False):
|
|
||||||
inject = utils.combine_vars(inject, self.runner.inventory.get_variables(self.runner.accelerate_inventory_host))
|
|
||||||
else:
|
|
||||||
inject = utils.combine_vars(inject, self.runner.inventory.get_variables(self.host))
|
|
||||||
vvvv("attempting to start up the accelerate daemon...")
|
|
||||||
self.ssh.connect()
|
|
||||||
tmp_path = self.runner._make_tmp_path(self.ssh)
|
|
||||||
return self.runner._execute_module(self.ssh, tmp_path, 'accelerate', args, inject=inject)
|
|
||||||
|
|
||||||
def connect(self, allow_ssh=True):
|
|
||||||
''' activates the connection object '''
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
wrong_user = False
|
|
||||||
tries = 3
|
|
||||||
self.conn = socket.socket()
|
|
||||||
self.conn.settimeout(constants.ACCELERATE_CONNECT_TIMEOUT)
|
|
||||||
vvvv("attempting connection to %s via the accelerated port %d" % (self.host,self.accport))
|
|
||||||
while tries > 0:
|
|
||||||
try:
|
|
||||||
self.conn.connect((self.host,self.accport))
|
|
||||||
break
|
|
||||||
except socket.error:
|
|
||||||
vvvv("connection to %s failed, retrying..." % self.host)
|
|
||||||
time.sleep(0.1)
|
|
||||||
tries -= 1
|
|
||||||
if tries == 0:
|
|
||||||
vvv("Could not connect via the accelerated connection, exceeded # of tries")
|
|
||||||
raise AnsibleError("FAILED")
|
|
||||||
elif wrong_user:
|
|
||||||
vvv("Restarting daemon with a different remote_user")
|
|
||||||
raise AnsibleError("WRONG_USER")
|
|
||||||
|
|
||||||
self.conn.settimeout(constants.ACCELERATE_TIMEOUT)
|
|
||||||
if not self.validate_user():
|
|
||||||
# the accelerated daemon was started with a
|
|
||||||
# different remote_user. The above command
|
|
||||||
# should have caused the accelerate daemon to
|
|
||||||
# shutdown, so we'll reconnect.
|
|
||||||
wrong_user = True
|
|
||||||
|
|
||||||
except AnsibleError, e:
|
|
||||||
if allow_ssh:
|
|
||||||
if "WRONG_USER" in e:
|
|
||||||
vvv("Switching users, waiting for the daemon on %s to shutdown completely..." % self.host)
|
|
||||||
time.sleep(5)
|
|
||||||
vvv("Falling back to ssh to startup accelerated mode")
|
|
||||||
res = self._execute_accelerate_module()
|
|
||||||
if not res.is_successful():
|
|
||||||
raise AnsibleError("Failed to launch the accelerated daemon on %s (reason: %s)" % (self.host,res.result.get('msg')))
|
|
||||||
return self.connect(allow_ssh=False)
|
|
||||||
else:
|
|
||||||
raise AnsibleError("Failed to connect to %s:%s" % (self.host,self.accport))
|
|
||||||
self.is_connected = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def send_data(self, data):
|
|
||||||
packed_len = struct.pack('!Q',len(data))
|
|
||||||
return self.conn.sendall(packed_len + data)
|
|
||||||
|
|
||||||
def recv_data(self):
|
|
||||||
header_len = 8 # size of a packed unsigned long long
|
|
||||||
data = b""
|
|
||||||
try:
|
|
||||||
vvvv("%s: in recv_data(), waiting for the header" % self.host)
|
|
||||||
while len(data) < header_len:
|
|
||||||
d = self.conn.recv(header_len - len(data))
|
|
||||||
if not d:
|
|
||||||
vvvv("%s: received nothing, bailing out" % self.host)
|
|
||||||
return None
|
|
||||||
data += d
|
|
||||||
vvvv("%s: got the header, unpacking" % self.host)
|
|
||||||
data_len = struct.unpack('!Q',data[:header_len])[0]
|
|
||||||
data = data[header_len:]
|
|
||||||
vvvv("%s: data received so far (expecting %d): %d" % (self.host,data_len,len(data)))
|
|
||||||
while len(data) < data_len:
|
|
||||||
d = self.conn.recv(data_len - len(data))
|
|
||||||
if not d:
|
|
||||||
vvvv("%s: received nothing, bailing out" % self.host)
|
|
||||||
return None
|
|
||||||
vvvv("%s: received %d bytes" % (self.host, len(d)))
|
|
||||||
data += d
|
|
||||||
vvvv("%s: received all of the data, returning" % self.host)
|
|
||||||
return data
|
|
||||||
except socket.timeout:
|
|
||||||
raise AnsibleError("timed out while waiting to receive data")
|
|
||||||
|
|
||||||
def validate_user(self):
|
|
||||||
'''
|
|
||||||
Checks the remote uid of the accelerated daemon vs. the
|
|
||||||
one specified for this play and will cause the accel
|
|
||||||
daemon to exit if they don't match
|
|
||||||
'''
|
|
||||||
|
|
||||||
vvvv("%s: sending request for validate_user" % self.host)
|
|
||||||
data = dict(
|
|
||||||
mode='validate_user',
|
|
||||||
username=self.user,
|
|
||||||
)
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
if self.send_data(data):
|
|
||||||
raise AnsibleError("Failed to send command to %s" % self.host)
|
|
||||||
|
|
||||||
vvvv("%s: waiting for validate_user response" % self.host)
|
|
||||||
while True:
|
|
||||||
# we loop here while waiting for the response, because a
|
|
||||||
# long running command may cause us to receive keepalive packets
|
|
||||||
# ({"pong":"true"}) rather than the response we want.
|
|
||||||
response = self.recv_data()
|
|
||||||
if not response:
|
|
||||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
if "pong" in response:
|
|
||||||
# it's a keepalive, go back to waiting
|
|
||||||
vvvv("%s: received a keepalive packet" % self.host)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
vvvv("%s: received the validate_user response: %s" % (self.host, response))
|
|
||||||
break
|
|
||||||
|
|
||||||
if response.get('failed'):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return response.get('rc') == 0
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the remote host '''
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
if executable == "":
|
|
||||||
executable = constants.DEFAULT_EXECUTABLE
|
|
||||||
|
|
||||||
if self.runner.become and sudoable:
|
|
||||||
cmd, prompt, success_key = utils.make_become_cmd(cmd, become_user, executable, self.runner.become_method, '', self.runner.become_exe)
|
|
||||||
|
|
||||||
vvv("EXEC COMMAND %s" % cmd)
|
|
||||||
|
|
||||||
data = dict(
|
|
||||||
mode='command',
|
|
||||||
cmd=cmd,
|
|
||||||
tmp_path=tmp_path,
|
|
||||||
executable=executable,
|
|
||||||
)
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
if self.send_data(data):
|
|
||||||
raise AnsibleError("Failed to send command to %s" % self.host)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# we loop here while waiting for the response, because a
|
|
||||||
# long running command may cause us to receive keepalive packets
|
|
||||||
# ({"pong":"true"}) rather than the response we want.
|
|
||||||
response = self.recv_data()
|
|
||||||
if not response:
|
|
||||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
if "pong" in response:
|
|
||||||
# it's a keepalive, go back to waiting
|
|
||||||
vvvv("%s: received a keepalive packet" % self.host)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
vvvv("%s: received the response" % self.host)
|
|
||||||
break
|
|
||||||
|
|
||||||
return (response.get('rc',None), '', response.get('stdout',''), response.get('stderr',''))
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
|
|
||||||
''' transfer a file from local to remote '''
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
|
|
||||||
fd = file(in_path, 'rb')
|
|
||||||
fstat = os.stat(in_path)
|
|
||||||
try:
|
|
||||||
vvv("PUT file is %d bytes" % fstat.st_size)
|
|
||||||
last = False
|
|
||||||
while fd.tell() <= fstat.st_size and not last:
|
|
||||||
vvvv("file position currently %ld, file size is %ld" % (fd.tell(), fstat.st_size))
|
|
||||||
data = fd.read(CHUNK_SIZE)
|
|
||||||
if fd.tell() >= fstat.st_size:
|
|
||||||
last = True
|
|
||||||
data = dict(mode='put', data=base64.b64encode(data), out_path=out_path, last=last)
|
|
||||||
if self.runner.become:
|
|
||||||
data['user'] = self.runner.become_user
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
|
|
||||||
if self.send_data(data):
|
|
||||||
raise AnsibleError("failed to send the file to %s" % self.host)
|
|
||||||
|
|
||||||
response = self.recv_data()
|
|
||||||
if not response:
|
|
||||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
|
|
||||||
if response.get('failed',False):
|
|
||||||
raise AnsibleError("failed to put the file in the requested location")
|
|
||||||
finally:
|
|
||||||
fd.close()
|
|
||||||
vvvv("waiting for final response after PUT")
|
|
||||||
response = self.recv_data()
|
|
||||||
if not response:
|
|
||||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
|
|
||||||
if response.get('failed',False):
|
|
||||||
raise AnsibleError("failed to put the file in the requested location")
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' save a remote file to the specified path '''
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
|
|
||||||
data = dict(mode='fetch', in_path=in_path)
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
if self.send_data(data):
|
|
||||||
raise AnsibleError("failed to initiate the file fetch with %s" % self.host)
|
|
||||||
|
|
||||||
fh = open(out_path, "w")
|
|
||||||
try:
|
|
||||||
bytes = 0
|
|
||||||
while True:
|
|
||||||
response = self.recv_data()
|
|
||||||
if not response:
|
|
||||||
raise AnsibleError("Failed to get a response from %s" % self.host)
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
if response.get('failed', False):
|
|
||||||
raise AnsibleError("Error during file fetch, aborting")
|
|
||||||
out = base64.b64decode(response['data'])
|
|
||||||
fh.write(out)
|
|
||||||
bytes += len(out)
|
|
||||||
# send an empty response back to signify we
|
|
||||||
# received the last chunk without errors
|
|
||||||
data = utils.jsonify(dict())
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
if self.send_data(data):
|
|
||||||
raise AnsibleError("failed to send ack during file fetch")
|
|
||||||
if response.get('last', False):
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
# we don't currently care about this final response,
|
|
||||||
# we just receive it and drop it. It may be used at some
|
|
||||||
# point in the future or we may just have the put/fetch
|
|
||||||
# operations not send back a final response at all
|
|
||||||
response = self.recv_data()
|
|
||||||
vvv("FETCH wrote %d bytes to %s" % (bytes, out_path))
|
|
||||||
fh.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection '''
|
|
||||||
# Be a good citizen
|
|
||||||
try:
|
|
||||||
self.conn.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
|||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# (c) 2013, Maykel Moya <mmoya@speedyrails.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/>.
|
|
||||||
|
|
||||||
import distutils.spawn
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
import ansible.constants as C
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' Local chroot based connections '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
self.chroot = host
|
|
||||||
self.has_pipelining = False
|
|
||||||
self.become_methods_supported=C.BECOME_METHODS
|
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise errors.AnsibleError("chroot connection requires running as root")
|
|
||||||
|
|
||||||
# we're running as root on the local system so do some
|
|
||||||
# trivial checks for ensuring 'host' is actually a chroot'able dir
|
|
||||||
if not os.path.isdir(self.chroot):
|
|
||||||
raise errors.AnsibleError("%s is not a directory" % self.chroot)
|
|
||||||
|
|
||||||
chrootsh = os.path.join(self.chroot, 'bin/sh')
|
|
||||||
if not utils.is_executable(chrootsh):
|
|
||||||
raise errors.AnsibleError("%s does not look like a chrootable dir (/bin/sh missing)" % self.chroot)
|
|
||||||
|
|
||||||
self.chroot_cmd = distutils.spawn.find_executable('chroot')
|
|
||||||
if not self.chroot_cmd:
|
|
||||||
raise errors.AnsibleError("chroot command not found in PATH")
|
|
||||||
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
# port is unused, since this is local
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
def connect(self, port=None):
|
|
||||||
''' connect to the chroot; nothing to do here '''
|
|
||||||
|
|
||||||
vvv("THIS IS A LOCAL CHROOT DIR", host=self.chroot)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the chroot '''
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
# We enter chroot as root so we ignore privlege escalation?
|
|
||||||
|
|
||||||
if executable:
|
|
||||||
local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
|
|
||||||
else:
|
|
||||||
local_cmd = '%s "%s" %s' % (self.chroot_cmd, self.chroot, cmd)
|
|
||||||
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.chroot)
|
|
||||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
return (p.returncode, '', stdout, stderr)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to chroot '''
|
|
||||||
|
|
||||||
if not out_path.startswith(os.path.sep):
|
|
||||||
out_path = os.path.join(os.path.sep, out_path)
|
|
||||||
normpath = os.path.normpath(out_path)
|
|
||||||
out_path = os.path.join(self.chroot, normpath[1:])
|
|
||||||
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.chroot)
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
try:
|
|
||||||
shutil.copyfile(in_path, out_path)
|
|
||||||
except shutil.Error:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
|
||||||
except IOError:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' fetch a file from chroot to local '''
|
|
||||||
|
|
||||||
if not in_path.startswith(os.path.sep):
|
|
||||||
in_path = os.path.join(os.path.sep, in_path)
|
|
||||||
normpath = os.path.normpath(in_path)
|
|
||||||
in_path = os.path.join(self.chroot, normpath[1:])
|
|
||||||
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.chroot)
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
try:
|
|
||||||
shutil.copyfile(in_path, out_path)
|
|
||||||
except shutil.Error:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
|
||||||
except IOError:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection; nothing to do here '''
|
|
||||||
pass
|
|
@ -1,153 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import constants
|
|
||||||
|
|
||||||
HAVE_ZMQ=False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import zmq
|
|
||||||
HAVE_ZMQ=True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' ZeroMQ accelerated connection '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
|
|
||||||
self.runner = runner
|
|
||||||
self.has_pipelining = False
|
|
||||||
|
|
||||||
# attempt to work around shared-memory funness
|
|
||||||
if getattr(self.runner, 'aes_keys', None):
|
|
||||||
utils.AES_KEYS = self.runner.aes_keys
|
|
||||||
|
|
||||||
self.host = host
|
|
||||||
self.key = utils.key_for_hostname(host)
|
|
||||||
self.context = None
|
|
||||||
self.socket = None
|
|
||||||
|
|
||||||
if port is None:
|
|
||||||
self.port = constants.ZEROMQ_PORT
|
|
||||||
else:
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
self.become_methods_supported=[]
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
''' activates the connection object '''
|
|
||||||
|
|
||||||
if not HAVE_ZMQ:
|
|
||||||
raise errors.AnsibleError("zmq is not installed")
|
|
||||||
|
|
||||||
# this is rough/temporary and will likely be optimized later ...
|
|
||||||
self.context = zmq.Context()
|
|
||||||
socket = self.context.socket(zmq.REQ)
|
|
||||||
addr = "tcp://%s:%s" % (self.host, self.port)
|
|
||||||
socket.connect(addr)
|
|
||||||
self.socket = socket
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the remote host '''
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
vvv("EXEC COMMAND %s" % cmd)
|
|
||||||
|
|
||||||
if self.runner.become and sudoable:
|
|
||||||
raise errors.AnsibleError(
|
|
||||||
"When using fireball, do not specify sudo or su to run your tasks. " +
|
|
||||||
"Instead sudo the fireball action with sudo. " +
|
|
||||||
"Task will communicate with the fireball already running in sudo mode."
|
|
||||||
)
|
|
||||||
|
|
||||||
data = dict(
|
|
||||||
mode='command',
|
|
||||||
cmd=cmd,
|
|
||||||
tmp_path=tmp_path,
|
|
||||||
executable=executable,
|
|
||||||
)
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
self.socket.send(data)
|
|
||||||
|
|
||||||
response = self.socket.recv()
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
|
|
||||||
return (response.get('rc',None), '', response.get('stdout',''), response.get('stderr',''))
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
|
|
||||||
''' transfer a file from local to remote '''
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
data = file(in_path).read()
|
|
||||||
data = base64.b64encode(data)
|
|
||||||
|
|
||||||
data = dict(mode='put', data=data, out_path=out_path)
|
|
||||||
# TODO: support chunked file transfer
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
self.socket.send(data)
|
|
||||||
|
|
||||||
response = self.socket.recv()
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
|
|
||||||
# no meaningful response needed for this
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' save a remote file to the specified path '''
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
|
|
||||||
data = dict(mode='fetch', in_path=in_path)
|
|
||||||
data = utils.jsonify(data)
|
|
||||||
data = utils.encrypt(self.key, data)
|
|
||||||
self.socket.send(data)
|
|
||||||
|
|
||||||
response = self.socket.recv()
|
|
||||||
response = utils.decrypt(self.key, response)
|
|
||||||
response = utils.parse_json(response)
|
|
||||||
response = response['data']
|
|
||||||
response = base64.b64decode(response)
|
|
||||||
|
|
||||||
fh = open(out_path, "w")
|
|
||||||
fh.write(response)
|
|
||||||
fh.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection '''
|
|
||||||
# Be a good citizen
|
|
||||||
try:
|
|
||||||
self.socket.close()
|
|
||||||
self.context.term()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
|||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
|
||||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# ---
|
|
||||||
# The func transport permit to use ansible over func. For people who have already setup
|
|
||||||
# func and that wish to play with ansible, this permit to move gradually to ansible
|
|
||||||
# without having to redo completely the setup of the network.
|
|
||||||
|
|
||||||
HAVE_FUNC=False
|
|
||||||
try:
|
|
||||||
import func.overlord.client as fc
|
|
||||||
HAVE_FUNC=True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
import os
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
from ansible import errors
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' Func-based connections '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.has_pipelining = False
|
|
||||||
# port is unused, this go on func
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
def connect(self, port=None):
|
|
||||||
if not HAVE_FUNC:
|
|
||||||
raise errors.AnsibleError("func is not installed")
|
|
||||||
|
|
||||||
self.client = fc.Client(self.host)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False,
|
|
||||||
executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the remote minion '''
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
# totally ignores privlege escalation
|
|
||||||
vvv("EXEC %s" % (cmd), host=self.host)
|
|
||||||
p = self.client.command.run(cmd)[self.host]
|
|
||||||
return (p[0], '', p[1], p[2])
|
|
||||||
|
|
||||||
def _normalize_path(self, path, prefix):
|
|
||||||
if not path.startswith(os.path.sep):
|
|
||||||
path = os.path.join(os.path.sep, path)
|
|
||||||
normpath = os.path.normpath(path)
|
|
||||||
return os.path.join(prefix, normpath[1:])
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to remote '''
|
|
||||||
|
|
||||||
out_path = self._normalize_path(out_path, '/')
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
self.client.local.copyfile.send(in_path, out_path)
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' fetch a file from remote to local '''
|
|
||||||
|
|
||||||
in_path = self._normalize_path(in_path, '/')
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
# need to use a tmp dir due to difference of semantic for getfile
|
|
||||||
# ( who take a # directory as destination) and fetch_file, who
|
|
||||||
# take a file directly
|
|
||||||
tmpdir = tempfile.mkdtemp(prefix="func_ansible")
|
|
||||||
self.client.local.getfile.get(in_path, tmpdir)
|
|
||||||
shutil.move(os.path.join(tmpdir, self.host, os.path.basename(in_path)),
|
|
||||||
out_path)
|
|
||||||
shutil.rmtree(tmpdir)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection; nothing to do here '''
|
|
||||||
pass
|
|
@ -1,153 +0,0 @@
|
|||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
|
||||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import distutils.spawn
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
import ansible.constants as C
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' Local chroot based connections '''
|
|
||||||
|
|
||||||
def _search_executable(self, executable):
|
|
||||||
cmd = distutils.spawn.find_executable(executable)
|
|
||||||
if not cmd:
|
|
||||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def list_jails(self):
|
|
||||||
p = subprocess.Popen([self.jls_cmd, '-q', 'name'],
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
|
|
||||||
return stdout.split()
|
|
||||||
|
|
||||||
def get_jail_path(self):
|
|
||||||
p = subprocess.Popen([self.jls_cmd, '-j', self.jail, '-q', 'path'],
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
# remove \n
|
|
||||||
return stdout[:-1]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
self.jail = host
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.has_pipelining = False
|
|
||||||
self.become_methods_supported=C.BECOME_METHODS
|
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise errors.AnsibleError("jail connection requires running as root")
|
|
||||||
|
|
||||||
self.jls_cmd = self._search_executable('jls')
|
|
||||||
self.jexec_cmd = self._search_executable('jexec')
|
|
||||||
|
|
||||||
if not self.jail in self.list_jails():
|
|
||||||
raise errors.AnsibleError("incorrect jail name %s" % self.jail)
|
|
||||||
|
|
||||||
|
|
||||||
self.host = host
|
|
||||||
# port is unused, since this is local
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
def connect(self, port=None):
|
|
||||||
''' connect to the chroot; nothing to do here '''
|
|
||||||
|
|
||||||
vvv("THIS IS A LOCAL CHROOT DIR", host=self.jail)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
# a modifier
|
|
||||||
def _generate_cmd(self, executable, cmd):
|
|
||||||
if executable:
|
|
||||||
local_cmd = [self.jexec_cmd, self.jail, executable, '-c', cmd]
|
|
||||||
else:
|
|
||||||
local_cmd = '%s "%s" %s' % (self.jexec_cmd, self.jail, cmd)
|
|
||||||
return local_cmd
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the chroot '''
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
# Ignores privilege escalation
|
|
||||||
local_cmd = self._generate_cmd(executable, cmd)
|
|
||||||
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.jail)
|
|
||||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
return (p.returncode, '', stdout, stderr)
|
|
||||||
|
|
||||||
def _normalize_path(self, path, prefix):
|
|
||||||
if not path.startswith(os.path.sep):
|
|
||||||
path = os.path.join(os.path.sep, path)
|
|
||||||
normpath = os.path.normpath(path)
|
|
||||||
return os.path.join(prefix, normpath[1:])
|
|
||||||
|
|
||||||
def _copy_file(self, in_path, out_path):
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
try:
|
|
||||||
shutil.copyfile(in_path, out_path)
|
|
||||||
except shutil.Error:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
|
||||||
except IOError:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to chroot '''
|
|
||||||
|
|
||||||
out_path = self._normalize_path(out_path, self.get_jail_path())
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.jail)
|
|
||||||
|
|
||||||
self._copy_file(in_path, out_path)
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' fetch a file from chroot to local '''
|
|
||||||
|
|
||||||
in_path = self._normalize_path(in_path, self.get_jail_path())
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.jail)
|
|
||||||
|
|
||||||
self._copy_file(in_path, out_path)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection; nothing to do here '''
|
|
||||||
pass
|
|
@ -1,129 +0,0 @@
|
|||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
|
||||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import distutils.spawn
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
import ansible.constants as C
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' Local lxc based connections '''
|
|
||||||
|
|
||||||
def _search_executable(self, executable):
|
|
||||||
cmd = distutils.spawn.find_executable(executable)
|
|
||||||
if not cmd:
|
|
||||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def _check_domain(self, domain):
|
|
||||||
p = subprocess.Popen([self.cmd, '-q', '-c', 'lxc:///', 'dominfo', domain],
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
p.communicate()
|
|
||||||
if p.returncode:
|
|
||||||
raise errors.AnsibleError("%s is not a lxc defined in libvirt" % domain)
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
self.lxc = host
|
|
||||||
|
|
||||||
self.cmd = self._search_executable('virsh')
|
|
||||||
|
|
||||||
self._check_domain(host)
|
|
||||||
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
# port is unused, since this is local
|
|
||||||
self.port = port
|
|
||||||
self.become_methods_supported=C.BECOME_METHODS
|
|
||||||
|
|
||||||
def connect(self, port=None):
|
|
||||||
''' connect to the lxc; nothing to do here '''
|
|
||||||
|
|
||||||
vvv("THIS IS A LOCAL LXC DIR", host=self.lxc)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _generate_cmd(self, executable, cmd):
|
|
||||||
if executable:
|
|
||||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', executable , '-c', cmd]
|
|
||||||
else:
|
|
||||||
local_cmd = '%s -q -c lxc:/// lxc-enter-namespace %s -- %s' % (self.cmd, self.lxc, cmd)
|
|
||||||
return local_cmd
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the chroot '''
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
# We ignore privilege escalation!
|
|
||||||
local_cmd = self._generate_cmd(executable, cmd)
|
|
||||||
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
|
||||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
return (p.returncode, '', stdout, stderr)
|
|
||||||
|
|
||||||
def _normalize_path(self, path, prefix):
|
|
||||||
if not path.startswith(os.path.sep):
|
|
||||||
path = os.path.join(os.path.sep, path)
|
|
||||||
normpath = os.path.normpath(path)
|
|
||||||
return os.path.join(prefix, normpath[1:])
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to lxc '''
|
|
||||||
|
|
||||||
out_path = self._normalize_path(out_path, '/')
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.lxc)
|
|
||||||
|
|
||||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', '/bin/tee', out_path]
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
|
||||||
|
|
||||||
p = subprocess.Popen(local_cmd, cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdout, stderr = p.communicate(open(in_path,'rb').read())
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' fetch a file from lxc to local '''
|
|
||||||
|
|
||||||
in_path = self._normalize_path(in_path, '/')
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.lxc)
|
|
||||||
|
|
||||||
local_cmd = [self.cmd, '-q', '-c', 'lxc:///', 'lxc-enter-namespace', self.lxc, '--', '/bin/cat', in_path]
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.lxc)
|
|
||||||
|
|
||||||
p = subprocess.Popen(local_cmd, cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
open(out_path,'wb').write(stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection; nothing to do here '''
|
|
||||||
pass
|
|
@ -1,129 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
import pipes
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import select
|
|
||||||
import fcntl
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' Local based connections '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
# port is unused, since this is local
|
|
||||||
self.port = port
|
|
||||||
self.has_pipelining = False
|
|
||||||
|
|
||||||
# TODO: add su(needs tty), pbrun, pfexec
|
|
||||||
self.become_methods_supported=['sudo']
|
|
||||||
|
|
||||||
def connect(self, port=None):
|
|
||||||
''' connect to the local host; nothing to do here '''
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the local host '''
|
|
||||||
|
|
||||||
# su requires to be run from a terminal, and therefore isn't supported here (yet?)
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
if self.runner.become and sudoable:
|
|
||||||
local_cmd, prompt, success_key = utils.make_become_cmd(cmd, become_user, executable, self.runner.become_method, '-H', self.runner.become_exe)
|
|
||||||
else:
|
|
||||||
if executable:
|
|
||||||
local_cmd = executable.split() + ['-c', cmd]
|
|
||||||
else:
|
|
||||||
local_cmd = cmd
|
|
||||||
executable = executable.split()[0] if executable else None
|
|
||||||
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.host)
|
|
||||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
|
||||||
cwd=self.runner.basedir, executable=executable,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
if self.runner.become and sudoable and self.runner.become_pass:
|
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
||||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL,
|
|
||||||
fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
||||||
become_output = ''
|
|
||||||
while success_key not in become_output:
|
|
||||||
|
|
||||||
if prompt and become_output.endswith(prompt):
|
|
||||||
break
|
|
||||||
if utils.su_prompts.check_su_prompt(become_output):
|
|
||||||
break
|
|
||||||
|
|
||||||
rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
|
|
||||||
[p.stdout, p.stderr], self.runner.timeout)
|
|
||||||
if p.stdout in rfd:
|
|
||||||
chunk = p.stdout.read()
|
|
||||||
elif p.stderr in rfd:
|
|
||||||
chunk = p.stderr.read()
|
|
||||||
else:
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
raise errors.AnsibleError('timeout waiting for %s password prompt:\n' % self.runner.become_method + become_output)
|
|
||||||
if not chunk:
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
raise errors.AnsibleError('%s output closed while waiting for password prompt:\n' % self.runner.become_method + become_output)
|
|
||||||
become_output += chunk
|
|
||||||
if success_key not in become_output:
|
|
||||||
p.stdin.write(self.runner.become_pass + '\n')
|
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
|
||||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
return (p.returncode, '', stdout, stderr)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to local '''
|
|
||||||
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
try:
|
|
||||||
shutil.copyfile(in_path, out_path)
|
|
||||||
except shutil.Error:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
|
||||||
except IOError:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
''' fetch a file from local to local -- for copatibility '''
|
|
||||||
self.put_file(in_path, out_path)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection; nothing to do here '''
|
|
||||||
pass
|
|
@ -1,419 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
|
|
||||||
# ---
|
|
||||||
# The paramiko transport is provided because many distributions, in particular EL6 and before
|
|
||||||
# do not support ControlPersist in their SSH implementations. This is needed on the Ansible
|
|
||||||
# control machine to be reasonably efficient with connections. Thus paramiko is faster
|
|
||||||
# for most users on these platforms. Users with ControlPersist capability can consider
|
|
||||||
# using -c ssh or configuring the transport in ansible.cfg.
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
import os
|
|
||||||
import pipes
|
|
||||||
import socket
|
|
||||||
import random
|
|
||||||
import logging
|
|
||||||
import tempfile
|
|
||||||
import traceback
|
|
||||||
import fcntl
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from termios import tcflush, TCIFLUSH
|
|
||||||
from binascii import hexlify
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
from ansible import constants as C
|
|
||||||
|
|
||||||
AUTHENTICITY_MSG="""
|
|
||||||
paramiko: The authenticity of host '%s' can't be established.
|
|
||||||
The %s key fingerprint is %s.
|
|
||||||
Are you sure you want to continue connecting (yes/no)?
|
|
||||||
"""
|
|
||||||
|
|
||||||
# prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/
|
|
||||||
HAVE_PARAMIKO=False
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.simplefilter("ignore")
|
|
||||||
try:
|
|
||||||
import paramiko
|
|
||||||
HAVE_PARAMIKO=True
|
|
||||||
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MyAddPolicy(object):
|
|
||||||
"""
|
|
||||||
Based on AutoAddPolicy in paramiko so we can determine when keys are added
|
|
||||||
and also prompt for input.
|
|
||||||
|
|
||||||
Policy for automatically adding the hostname and new host key to the
|
|
||||||
local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, runner):
|
|
||||||
self.runner = runner
|
|
||||||
|
|
||||||
def missing_host_key(self, client, hostname, key):
|
|
||||||
|
|
||||||
if C.HOST_KEY_CHECKING:
|
|
||||||
|
|
||||||
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
|
||||||
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
|
||||||
|
|
||||||
old_stdin = sys.stdin
|
|
||||||
sys.stdin = self.runner._new_stdin
|
|
||||||
fingerprint = hexlify(key.get_fingerprint())
|
|
||||||
ktype = key.get_name()
|
|
||||||
|
|
||||||
# clear out any premature input on sys.stdin
|
|
||||||
tcflush(sys.stdin, TCIFLUSH)
|
|
||||||
|
|
||||||
inp = raw_input(AUTHENTICITY_MSG % (hostname, ktype, fingerprint))
|
|
||||||
sys.stdin = old_stdin
|
|
||||||
if inp not in ['yes','y','']:
|
|
||||||
fcntl.flock(self.runner.output_lockfile, fcntl.LOCK_UN)
|
|
||||||
fcntl.flock(self.runner.process_lockfile, fcntl.LOCK_UN)
|
|
||||||
raise errors.AnsibleError("host connection rejected by user")
|
|
||||||
|
|
||||||
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
|
|
||||||
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
|
||||||
|
|
||||||
|
|
||||||
key._added_by_ansible_this_time = True
|
|
||||||
|
|
||||||
# existing implementation below:
|
|
||||||
client._host_keys.add(hostname, key.get_name(), key)
|
|
||||||
|
|
||||||
# host keys are actually saved in close() function below
|
|
||||||
# in order to control ordering.
|
|
||||||
|
|
||||||
|
|
||||||
# keep connection objects on a per host basis to avoid repeated attempts to reconnect
|
|
||||||
|
|
||||||
SSH_CONNECTION_CACHE = {}
|
|
||||||
SFTP_CONNECTION_CACHE = {}
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' SSH based connections with Paramiko '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
|
|
||||||
|
|
||||||
self.ssh = None
|
|
||||||
self.sftp = None
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.port = port or 22
|
|
||||||
self.user = user
|
|
||||||
self.password = password
|
|
||||||
self.private_key_file = private_key_file
|
|
||||||
self.has_pipelining = False
|
|
||||||
|
|
||||||
# TODO: add pbrun, pfexec
|
|
||||||
self.become_methods_supported=['sudo', 'su', 'pbrun']
|
|
||||||
|
|
||||||
def _cache_key(self):
|
|
||||||
return "%s__%s__" % (self.host, self.user)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
cache_key = self._cache_key()
|
|
||||||
if cache_key in SSH_CONNECTION_CACHE:
|
|
||||||
self.ssh = SSH_CONNECTION_CACHE[cache_key]
|
|
||||||
else:
|
|
||||||
self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _connect_uncached(self):
|
|
||||||
''' activates the connection object '''
|
|
||||||
|
|
||||||
if not HAVE_PARAMIKO:
|
|
||||||
raise errors.AnsibleError("paramiko is not installed")
|
|
||||||
|
|
||||||
vvv("ESTABLISH CONNECTION FOR USER: %s on PORT %s TO %s" % (self.user, self.port, self.host), host=self.host)
|
|
||||||
|
|
||||||
ssh = paramiko.SSHClient()
|
|
||||||
|
|
||||||
self.keyfile = os.path.expanduser("~/.ssh/known_hosts")
|
|
||||||
|
|
||||||
if C.HOST_KEY_CHECKING:
|
|
||||||
ssh.load_system_host_keys()
|
|
||||||
|
|
||||||
ssh.set_missing_host_key_policy(MyAddPolicy(self.runner))
|
|
||||||
|
|
||||||
allow_agent = True
|
|
||||||
|
|
||||||
if self.password is not None:
|
|
||||||
allow_agent = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
if self.private_key_file:
|
|
||||||
key_filename = os.path.expanduser(self.private_key_file)
|
|
||||||
elif self.runner.private_key_file:
|
|
||||||
key_filename = os.path.expanduser(self.runner.private_key_file)
|
|
||||||
else:
|
|
||||||
key_filename = None
|
|
||||||
ssh.connect(self.host, username=self.user, allow_agent=allow_agent, look_for_keys=True,
|
|
||||||
key_filename=key_filename, password=self.password,
|
|
||||||
timeout=self.runner.timeout, port=self.port)
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
|
|
||||||
msg = str(e)
|
|
||||||
if "PID check failed" in msg:
|
|
||||||
raise errors.AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible")
|
|
||||||
elif "Private key file is encrypted" in msg:
|
|
||||||
msg = 'ssh %s@%s:%s : %s\nTo connect as a different user, use -u <username>.' % (
|
|
||||||
self.user, self.host, self.port, msg)
|
|
||||||
raise errors.AnsibleConnectionFailed(msg)
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleConnectionFailed(msg)
|
|
||||||
|
|
||||||
return ssh
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the remote host '''
|
|
||||||
|
|
||||||
if self.runner.become and sudoable and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
bufsize = 4096
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
self.ssh.get_transport().set_keepalive(5)
|
|
||||||
chan = self.ssh.get_transport().open_session()
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
|
|
||||||
msg = "Failed to open session"
|
|
||||||
if len(str(e)) > 0:
|
|
||||||
msg += ": %s" % str(e)
|
|
||||||
raise errors.AnsibleConnectionFailed(msg)
|
|
||||||
|
|
||||||
no_prompt_out = ''
|
|
||||||
no_prompt_err = ''
|
|
||||||
if not (self.runner.become and sudoable):
|
|
||||||
|
|
||||||
if executable:
|
|
||||||
quoted_command = executable + ' -c ' + pipes.quote(cmd)
|
|
||||||
else:
|
|
||||||
quoted_command = cmd
|
|
||||||
vvv("EXEC %s" % quoted_command, host=self.host)
|
|
||||||
chan.exec_command(quoted_command)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
# sudo usually requires a PTY (cf. requiretty option), therefore
|
|
||||||
# we give it one by default (pty=True in ansble.cfg), and we try
|
|
||||||
# to initialise from the calling environment
|
|
||||||
if C.PARAMIKO_PTY:
|
|
||||||
chan.get_pty(term=os.getenv('TERM', 'vt100'),
|
|
||||||
width=int(os.getenv('COLUMNS', 0)),
|
|
||||||
height=int(os.getenv('LINES', 0)))
|
|
||||||
if self.runner.become and sudoable:
|
|
||||||
shcmd, prompt, success_key = utils.make_become_cmd(cmd, become_user, executable, self.runner.become_method, '', self.runner.become_exe)
|
|
||||||
|
|
||||||
vvv("EXEC %s" % shcmd, host=self.host)
|
|
||||||
become_output = ''
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
chan.exec_command(shcmd)
|
|
||||||
|
|
||||||
if self.runner.become_pass:
|
|
||||||
|
|
||||||
while True:
|
|
||||||
|
|
||||||
if success_key in become_output or \
|
|
||||||
(prompt and become_output.endswith(prompt)) or \
|
|
||||||
utils.su_prompts.check_su_prompt(become_output):
|
|
||||||
break
|
|
||||||
chunk = chan.recv(bufsize)
|
|
||||||
|
|
||||||
if not chunk:
|
|
||||||
if 'unknown user' in become_output:
|
|
||||||
raise errors.AnsibleError(
|
|
||||||
'user %s does not exist' % become_user)
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleError('ssh connection ' +
|
|
||||||
'closed waiting for password prompt')
|
|
||||||
become_output += chunk
|
|
||||||
|
|
||||||
if success_key not in become_output:
|
|
||||||
|
|
||||||
if sudoable:
|
|
||||||
chan.sendall(self.runner.become_pass + '\n')
|
|
||||||
else:
|
|
||||||
no_prompt_out += become_output
|
|
||||||
no_prompt_err += become_output
|
|
||||||
|
|
||||||
except socket.timeout:
|
|
||||||
|
|
||||||
raise errors.AnsibleError('ssh timed out waiting for privilege escalation.\n' + become_output)
|
|
||||||
|
|
||||||
stdout = ''.join(chan.makefile('rb', bufsize))
|
|
||||||
stderr = ''.join(chan.makefile_stderr('rb', bufsize))
|
|
||||||
|
|
||||||
return (chan.recv_exit_status(), '', no_prompt_out + stdout, no_prompt_out + stderr)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to remote '''
|
|
||||||
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.sftp = self.ssh.open_sftp()
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError("failed to open a SFTP connection (%s)" % e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.sftp.put(in_path, out_path)
|
|
||||||
except IOError:
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def _connect_sftp(self):
|
|
||||||
|
|
||||||
cache_key = "%s__%s__" % (self.host, self.user)
|
|
||||||
if cache_key in SFTP_CONNECTION_CACHE:
|
|
||||||
return SFTP_CONNECTION_CACHE[cache_key]
|
|
||||||
else:
|
|
||||||
result = SFTP_CONNECTION_CACHE[cache_key] = self.connect().ssh.open_sftp()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' save a remote file to the specified path '''
|
|
||||||
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.sftp = self._connect_sftp()
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError("failed to open a SFTP connection (%s)", e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.sftp.get(in_path, out_path)
|
|
||||||
except IOError:
|
|
||||||
raise errors.AnsibleError("failed to transfer file from %s" % in_path)
|
|
||||||
|
|
||||||
def _any_keys_added(self):
|
|
||||||
|
|
||||||
added_any = False
|
|
||||||
for hostname, keys in self.ssh._host_keys.iteritems():
|
|
||||||
for keytype, key in keys.iteritems():
|
|
||||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
|
||||||
if added_this_time:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _save_ssh_host_keys(self, filename):
|
|
||||||
'''
|
|
||||||
not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
|
|
||||||
don't complain about it :)
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not self._any_keys_added():
|
|
||||||
return False
|
|
||||||
|
|
||||||
path = os.path.expanduser("~/.ssh")
|
|
||||||
if not os.path.exists(path):
|
|
||||||
os.makedirs(path)
|
|
||||||
|
|
||||||
f = open(filename, 'w')
|
|
||||||
|
|
||||||
for hostname, keys in self.ssh._host_keys.iteritems():
|
|
||||||
|
|
||||||
for keytype, key in keys.iteritems():
|
|
||||||
|
|
||||||
# was f.write
|
|
||||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
|
||||||
if not added_this_time:
|
|
||||||
f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
|
|
||||||
|
|
||||||
for hostname, keys in self.ssh._host_keys.iteritems():
|
|
||||||
|
|
||||||
for keytype, key in keys.iteritems():
|
|
||||||
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
|
||||||
if added_this_time:
|
|
||||||
f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
|
|
||||||
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection '''
|
|
||||||
|
|
||||||
cache_key = self._cache_key()
|
|
||||||
SSH_CONNECTION_CACHE.pop(cache_key, None)
|
|
||||||
SFTP_CONNECTION_CACHE.pop(cache_key, None)
|
|
||||||
|
|
||||||
if self.sftp is not None:
|
|
||||||
self.sftp.close()
|
|
||||||
|
|
||||||
if C.HOST_KEY_CHECKING and C.PARAMIKO_RECORD_HOST_KEYS and self._any_keys_added():
|
|
||||||
|
|
||||||
# add any new SSH host keys -- warning -- this could be slow
|
|
||||||
lockfile = self.keyfile.replace("known_hosts",".known_hosts.lock")
|
|
||||||
dirname = os.path.dirname(self.keyfile)
|
|
||||||
if not os.path.exists(dirname):
|
|
||||||
os.makedirs(dirname)
|
|
||||||
|
|
||||||
KEY_LOCK = open(lockfile, 'w')
|
|
||||||
fcntl.lockf(KEY_LOCK, fcntl.LOCK_EX)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# just in case any were added recently
|
|
||||||
|
|
||||||
self.ssh.load_system_host_keys()
|
|
||||||
self.ssh._host_keys.update(self.ssh._system_host_keys)
|
|
||||||
|
|
||||||
# gather information about the current key file, so
|
|
||||||
# we can ensure the new file has the correct mode/owner
|
|
||||||
|
|
||||||
key_dir = os.path.dirname(self.keyfile)
|
|
||||||
key_stat = os.stat(self.keyfile)
|
|
||||||
|
|
||||||
# Save the new keys to a temporary file and move it into place
|
|
||||||
# rather than rewriting the file. We set delete=False because
|
|
||||||
# the file will be moved into place rather than cleaned up.
|
|
||||||
|
|
||||||
tmp_keyfile = tempfile.NamedTemporaryFile(dir=key_dir, delete=False)
|
|
||||||
os.chmod(tmp_keyfile.name, key_stat.st_mode & 07777)
|
|
||||||
os.chown(tmp_keyfile.name, key_stat.st_uid, key_stat.st_gid)
|
|
||||||
|
|
||||||
self._save_ssh_host_keys(tmp_keyfile.name)
|
|
||||||
tmp_keyfile.close()
|
|
||||||
|
|
||||||
os.rename(tmp_keyfile.name, self.keyfile)
|
|
||||||
|
|
||||||
except:
|
|
||||||
|
|
||||||
# unable to save keys, including scenario when key was invalid
|
|
||||||
# and caught earlier
|
|
||||||
traceback.print_exc()
|
|
||||||
pass
|
|
||||||
fcntl.lockf(KEY_LOCK, fcntl.LOCK_UN)
|
|
||||||
|
|
||||||
self.ssh.close()
|
|
||||||
|
|
@ -1,460 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import shlex
|
|
||||||
import pipes
|
|
||||||
import random
|
|
||||||
import select
|
|
||||||
import fcntl
|
|
||||||
import hmac
|
|
||||||
import pwd
|
|
||||||
import gettext
|
|
||||||
import pty
|
|
||||||
from hashlib import sha1
|
|
||||||
import ansible.constants as C
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' ssh based connections '''
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.ipv6 = ':' in self.host
|
|
||||||
self.port = port
|
|
||||||
self.user = str(user)
|
|
||||||
self.password = password
|
|
||||||
self.private_key_file = private_key_file
|
|
||||||
self.HASHED_KEY_MAGIC = "|1|"
|
|
||||||
self.has_pipelining = True
|
|
||||||
|
|
||||||
# TODO: add pbrun, pfexec
|
|
||||||
self.become_methods_supported=['sudo', 'su', 'pbrun']
|
|
||||||
|
|
||||||
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
|
||||||
self.cp_dir = utils.prepare_writeable_dir('$HOME/.ansible/cp',mode=0700)
|
|
||||||
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
''' connect to the remote host '''
|
|
||||||
|
|
||||||
vvv("ESTABLISH CONNECTION FOR USER: %s" % self.user, host=self.host)
|
|
||||||
|
|
||||||
self.common_args = []
|
|
||||||
extra_args = C.ANSIBLE_SSH_ARGS
|
|
||||||
if extra_args is not None:
|
|
||||||
# make sure there is no empty string added as this can produce weird errors
|
|
||||||
self.common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()]
|
|
||||||
else:
|
|
||||||
self.common_args += ["-o", "ControlMaster=auto",
|
|
||||||
"-o", "ControlPersist=60s",
|
|
||||||
"-o", "ControlPath=\"%s\"" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
|
|
||||||
|
|
||||||
cp_in_use = False
|
|
||||||
cp_path_set = False
|
|
||||||
for arg in self.common_args:
|
|
||||||
if "ControlPersist" in arg:
|
|
||||||
cp_in_use = True
|
|
||||||
if "ControlPath" in arg:
|
|
||||||
cp_path_set = True
|
|
||||||
|
|
||||||
if cp_in_use and not cp_path_set:
|
|
||||||
self.common_args += ["-o", "ControlPath=\"%s\"" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
|
|
||||||
|
|
||||||
if not C.HOST_KEY_CHECKING:
|
|
||||||
self.common_args += ["-o", "StrictHostKeyChecking=no"]
|
|
||||||
|
|
||||||
if self.port is not None:
|
|
||||||
self.common_args += ["-o", "Port=%d" % (self.port)]
|
|
||||||
if self.private_key_file is not None:
|
|
||||||
self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.private_key_file)]
|
|
||||||
elif self.runner.private_key_file is not None:
|
|
||||||
self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.runner.private_key_file)]
|
|
||||||
if self.password:
|
|
||||||
self.common_args += ["-o", "GSSAPIAuthentication=no",
|
|
||||||
"-o", "PubkeyAuthentication=no"]
|
|
||||||
else:
|
|
||||||
self.common_args += ["-o", "KbdInteractiveAuthentication=no",
|
|
||||||
"-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
|
|
||||||
"-o", "PasswordAuthentication=no"]
|
|
||||||
if self.user != pwd.getpwuid(os.geteuid())[0]:
|
|
||||||
self.common_args += ["-o", "User="+self.user]
|
|
||||||
self.common_args += ["-o", "ConnectTimeout=%d" % self.runner.timeout]
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _run(self, cmd, indata):
|
|
||||||
if indata:
|
|
||||||
# do not use pseudo-pty
|
|
||||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdin = p.stdin
|
|
||||||
else:
|
|
||||||
# try to use upseudo-pty
|
|
||||||
try:
|
|
||||||
# Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
|
|
||||||
master, slave = pty.openpty()
|
|
||||||
p = subprocess.Popen(cmd, stdin=slave,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdin = os.fdopen(master, 'w', 0)
|
|
||||||
os.close(slave)
|
|
||||||
except:
|
|
||||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdin = p.stdin
|
|
||||||
|
|
||||||
return (p, stdin)
|
|
||||||
|
|
||||||
def _password_cmd(self):
|
|
||||||
if self.password:
|
|
||||||
try:
|
|
||||||
p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
p.communicate()
|
|
||||||
except OSError:
|
|
||||||
raise errors.AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
|
|
||||||
(self.rfd, self.wfd) = os.pipe()
|
|
||||||
return ["sshpass", "-d%d" % self.rfd]
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _send_password(self):
|
|
||||||
if self.password:
|
|
||||||
os.close(self.rfd)
|
|
||||||
os.write(self.wfd, "%s\n" % self.password)
|
|
||||||
os.close(self.wfd)
|
|
||||||
|
|
||||||
def _communicate(self, p, stdin, indata, sudoable=False, prompt=None):
|
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
|
||||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
|
||||||
# We can't use p.communicate here because the ControlMaster may have stdout open as well
|
|
||||||
stdout = ''
|
|
||||||
stderr = ''
|
|
||||||
rpipes = [p.stdout, p.stderr]
|
|
||||||
if indata:
|
|
||||||
try:
|
|
||||||
stdin.write(indata)
|
|
||||||
stdin.close()
|
|
||||||
except:
|
|
||||||
raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
|
|
||||||
# Read stdout/stderr from process
|
|
||||||
while True:
|
|
||||||
rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
|
|
||||||
|
|
||||||
# fail early if the become password is wrong
|
|
||||||
if self.runner.become and sudoable:
|
|
||||||
incorrect_password = gettext.dgettext(self.runner.become_method, C.BECOME_ERROR_STRINGS[self.runner.become_method])
|
|
||||||
|
|
||||||
if prompt:
|
|
||||||
if self.runner.become_pass:
|
|
||||||
if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
|
||||||
raise errors.AnsibleError('Incorrect become password')
|
|
||||||
|
|
||||||
if stdout.endswith(prompt):
|
|
||||||
raise errors.AnsibleError('Missing become password')
|
|
||||||
elif stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
|
|
||||||
raise errors.AnsibleError('Incorrect become password')
|
|
||||||
|
|
||||||
if p.stdout in rfd:
|
|
||||||
dat = os.read(p.stdout.fileno(), 9000)
|
|
||||||
stdout += dat
|
|
||||||
if dat == '':
|
|
||||||
rpipes.remove(p.stdout)
|
|
||||||
if p.stderr in rfd:
|
|
||||||
dat = os.read(p.stderr.fileno(), 9000)
|
|
||||||
stderr += dat
|
|
||||||
if dat == '':
|
|
||||||
rpipes.remove(p.stderr)
|
|
||||||
# only break out if no pipes are left to read or
|
|
||||||
# the pipes are completely read and
|
|
||||||
# the process is terminated
|
|
||||||
if (not rpipes or not rfd) and p.poll() is not None:
|
|
||||||
break
|
|
||||||
# No pipes are left to read but process is not yet terminated
|
|
||||||
# Only then it is safe to wait for the process to be finished
|
|
||||||
# NOTE: Actually p.poll() is always None here if rpipes is empty
|
|
||||||
elif not rpipes and p.poll() == None:
|
|
||||||
p.wait()
|
|
||||||
# The process is terminated. Since no pipes to read from are
|
|
||||||
# left, there is no need to call select() again.
|
|
||||||
break
|
|
||||||
# close stdin after process is terminated and stdout/stderr are read
|
|
||||||
# completely (see also issue #848)
|
|
||||||
stdin.close()
|
|
||||||
return (p.returncode, stdout, stderr)
|
|
||||||
|
|
||||||
def not_in_host_file(self, host):
|
|
||||||
if 'USER' in os.environ:
|
|
||||||
user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
|
|
||||||
else:
|
|
||||||
user_host_file = "~/.ssh/known_hosts"
|
|
||||||
user_host_file = os.path.expanduser(user_host_file)
|
|
||||||
|
|
||||||
host_file_list = []
|
|
||||||
host_file_list.append(user_host_file)
|
|
||||||
host_file_list.append("/etc/ssh/ssh_known_hosts")
|
|
||||||
host_file_list.append("/etc/ssh/ssh_known_hosts2")
|
|
||||||
|
|
||||||
hfiles_not_found = 0
|
|
||||||
for hf in host_file_list:
|
|
||||||
if not os.path.exists(hf):
|
|
||||||
hfiles_not_found += 1
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
host_fh = open(hf)
|
|
||||||
except IOError, e:
|
|
||||||
hfiles_not_found += 1
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
data = host_fh.read()
|
|
||||||
host_fh.close()
|
|
||||||
|
|
||||||
for line in data.split("\n"):
|
|
||||||
line = line.strip()
|
|
||||||
if line is None or " " not in line:
|
|
||||||
continue
|
|
||||||
tokens = line.split()
|
|
||||||
if not tokens:
|
|
||||||
continue
|
|
||||||
if tokens[0].find(self.HASHED_KEY_MAGIC) == 0:
|
|
||||||
# this is a hashed known host entry
|
|
||||||
try:
|
|
||||||
(kn_salt,kn_host) = tokens[0][len(self.HASHED_KEY_MAGIC):].split("|",2)
|
|
||||||
hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
|
|
||||||
hash.update(host)
|
|
||||||
if hash.digest() == kn_host.decode('base64'):
|
|
||||||
return False
|
|
||||||
except:
|
|
||||||
# invalid hashed host key, skip it
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# standard host file entry
|
|
||||||
if host in tokens[0]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (hfiles_not_found == len(host_file_list)):
|
|
||||||
vvv("EXEC previous known host file not found for %s" % host)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
|
|
||||||
''' run a command on the remote host '''
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
ssh_cmd = self._password_cmd()
|
|
||||||
ssh_cmd += ["ssh", "-C"]
|
|
||||||
if not in_data:
|
|
||||||
# we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
|
|
||||||
# inside a tty automatically invokes the python interactive-mode but the modules are not
|
|
||||||
# compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
|
|
||||||
ssh_cmd += ["-tt"]
|
|
||||||
if utils.VERBOSITY > 3:
|
|
||||||
ssh_cmd += ["-vvv"]
|
|
||||||
else:
|
|
||||||
if self.runner.module_name == 'raw':
|
|
||||||
ssh_cmd += ["-q"]
|
|
||||||
else:
|
|
||||||
ssh_cmd += ["-v"]
|
|
||||||
ssh_cmd += self.common_args
|
|
||||||
|
|
||||||
if self.ipv6:
|
|
||||||
ssh_cmd += ['-6']
|
|
||||||
ssh_cmd += [self.host]
|
|
||||||
|
|
||||||
if self.runner.become and sudoable:
|
|
||||||
becomecmd, prompt, success_key = utils.make_become_cmd(cmd, become_user, executable, self.runner.become_method, '', self.runner.become_exe)
|
|
||||||
ssh_cmd.append(becomecmd)
|
|
||||||
else:
|
|
||||||
prompt = None
|
|
||||||
if executable:
|
|
||||||
ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
|
|
||||||
else:
|
|
||||||
ssh_cmd.append(cmd)
|
|
||||||
|
|
||||||
vvv("EXEC %s" % ' '.join(ssh_cmd), host=self.host)
|
|
||||||
|
|
||||||
not_in_host_file = self.not_in_host_file(self.host)
|
|
||||||
|
|
||||||
if C.HOST_KEY_CHECKING and not_in_host_file:
|
|
||||||
# lock around the initial SSH connectivity so the user prompt about whether to add
|
|
||||||
# the host to known hosts is not intermingled with multiprocess output.
|
|
||||||
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
|
|
||||||
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
|
|
||||||
|
|
||||||
# create process
|
|
||||||
(p, stdin) = self._run(ssh_cmd, in_data)
|
|
||||||
|
|
||||||
self._send_password()
|
|
||||||
|
|
||||||
no_prompt_out = ''
|
|
||||||
no_prompt_err = ''
|
|
||||||
if sudoable and self.runner.become and self.runner.become_pass:
|
|
||||||
# several cases are handled for escalated privileges with password
|
|
||||||
# * NOPASSWD (tty & no-tty): detect success_key on stdout
|
|
||||||
# * without NOPASSWD:
|
|
||||||
# * detect prompt on stdout (tty)
|
|
||||||
# * detect prompt on stderr (no-tty)
|
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL,
|
|
||||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
||||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL,
|
|
||||||
fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
|
||||||
become_output = ''
|
|
||||||
become_errput = ''
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if success_key in become_output or \
|
|
||||||
(prompt and become_output.endswith(prompt)) or \
|
|
||||||
utils.su_prompts.check_su_prompt(become_output):
|
|
||||||
break
|
|
||||||
|
|
||||||
rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
|
|
||||||
[p.stdout], self.runner.timeout)
|
|
||||||
if p.stderr in rfd:
|
|
||||||
chunk = p.stderr.read()
|
|
||||||
if not chunk:
|
|
||||||
raise errors.AnsibleError('ssh connection closed waiting for a privilege escalation password prompt')
|
|
||||||
become_errput += chunk
|
|
||||||
incorrect_password = gettext.dgettext(
|
|
||||||
"become", "Sorry, try again.")
|
|
||||||
if become_errput.strip().endswith("%s%s" % (prompt, incorrect_password)):
|
|
||||||
raise errors.AnsibleError('Incorrect become password')
|
|
||||||
elif prompt and become_errput.endswith(prompt):
|
|
||||||
stdin.write(self.runner.become_pass + '\n')
|
|
||||||
|
|
||||||
if p.stdout in rfd:
|
|
||||||
chunk = p.stdout.read()
|
|
||||||
if not chunk:
|
|
||||||
raise errors.AnsibleError('ssh connection closed waiting for %s password prompt' % self.runner.become_method)
|
|
||||||
become_output += chunk
|
|
||||||
|
|
||||||
if not rfd:
|
|
||||||
# timeout. wrap up process communication
|
|
||||||
stdout = p.communicate()
|
|
||||||
raise errors.AnsibleError('ssh connection error while waiting for %s password prompt' % self.runner.become_method)
|
|
||||||
|
|
||||||
if success_key in become_output:
|
|
||||||
no_prompt_out += become_output
|
|
||||||
no_prompt_err += become_errput
|
|
||||||
elif sudoable:
|
|
||||||
stdin.write(self.runner.become_pass + '\n')
|
|
||||||
|
|
||||||
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, sudoable=sudoable, prompt=prompt)
|
|
||||||
|
|
||||||
if C.HOST_KEY_CHECKING and not_in_host_file:
|
|
||||||
# lock around the initial SSH connectivity so the user prompt about whether to add
|
|
||||||
# the host to known hosts is not intermingled with multiprocess output.
|
|
||||||
fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
|
|
||||||
fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
|
|
||||||
controlpersisterror = 'Bad configuration option: ControlPersist' in stderr or \
|
|
||||||
'unknown configuration option: ControlPersist' in stderr
|
|
||||||
|
|
||||||
if C.HOST_KEY_CHECKING:
|
|
||||||
if ssh_cmd[0] == "sshpass" and p.returncode == 6:
|
|
||||||
raise errors.AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host\'s fingerprint to your known_hosts file to manage this host.')
|
|
||||||
|
|
||||||
if p.returncode != 0 and controlpersisterror:
|
|
||||||
raise errors.AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" (or ssh_args in [ssh_connection] section of the config file) before running again')
|
|
||||||
if p.returncode == 255 and (in_data or self.runner.module_name == 'raw'):
|
|
||||||
raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
|
|
||||||
if p.returncode == 255:
|
|
||||||
ip = None
|
|
||||||
port = None
|
|
||||||
for line in stderr.splitlines():
|
|
||||||
match = re.search(
|
|
||||||
'Connecting to .*\[(\d+\.\d+\.\d+\.\d+)\] port (\d+)',
|
|
||||||
line)
|
|
||||||
if match:
|
|
||||||
ip = match.group(1)
|
|
||||||
port = match.group(2)
|
|
||||||
if 'UNPROTECTED PRIVATE KEY FILE' in stderr:
|
|
||||||
lines = [line for line in stderr.splitlines()
|
|
||||||
if 'ignore key:' in line]
|
|
||||||
else:
|
|
||||||
lines = stderr.splitlines()[-1:]
|
|
||||||
if ip and port:
|
|
||||||
lines.append(' while connecting to %s:%s' % (ip, port))
|
|
||||||
lines.append(
|
|
||||||
'It is sometimes useful to re-run the command using -vvvv, '
|
|
||||||
'which prints SSH debug output to help diagnose the issue.')
|
|
||||||
raise errors.AnsibleError('SSH Error: %s' % '\n'.join(lines))
|
|
||||||
|
|
||||||
return (p.returncode, '', no_prompt_out + stdout, no_prompt_err + stderr)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to remote '''
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
cmd = self._password_cmd()
|
|
||||||
|
|
||||||
host = self.host
|
|
||||||
if self.ipv6:
|
|
||||||
host = '[%s]' % host
|
|
||||||
|
|
||||||
if C.DEFAULT_SCP_IF_SSH:
|
|
||||||
cmd += ["scp"] + self.common_args
|
|
||||||
cmd += [in_path,host + ":" + pipes.quote(out_path)]
|
|
||||||
indata = None
|
|
||||||
else:
|
|
||||||
cmd += ["sftp"] + self.common_args + [host]
|
|
||||||
indata = "put %s %s\n" % (pipes.quote(in_path), pipes.quote(out_path))
|
|
||||||
|
|
||||||
(p, stdin) = self._run(cmd, indata)
|
|
||||||
|
|
||||||
self._send_password()
|
|
||||||
|
|
||||||
(returncode, stdout, stderr) = self._communicate(p, stdin, indata)
|
|
||||||
|
|
||||||
if returncode != 0:
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s:\n%s\n%s" % (out_path, stdout, stderr))
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' fetch a file from remote to local '''
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
cmd = self._password_cmd()
|
|
||||||
|
|
||||||
host = self.host
|
|
||||||
if self.ipv6:
|
|
||||||
host = '[%s]' % host
|
|
||||||
|
|
||||||
if C.DEFAULT_SCP_IF_SSH:
|
|
||||||
cmd += ["scp"] + self.common_args
|
|
||||||
cmd += [host + ":" + in_path, out_path]
|
|
||||||
indata = None
|
|
||||||
else:
|
|
||||||
cmd += ["sftp"] + self.common_args + [host]
|
|
||||||
indata = "get %s %s\n" % (in_path, out_path)
|
|
||||||
|
|
||||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
self._send_password()
|
|
||||||
stdout, stderr = p.communicate(indata)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
|
||||||
raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' not applicable since we're executing openssh binaries '''
|
|
||||||
pass
|
|
||||||
|
|
@ -1,270 +0,0 @@
|
|||||||
# (c) 2014, Chris Church <chris@ninemoreminutes.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 __future__ import absolute_import
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import traceback
|
|
||||||
import urlparse
|
|
||||||
from ansible import errors
|
|
||||||
from ansible import utils
|
|
||||||
from ansible.callbacks import vvv, vvvv, verbose
|
|
||||||
from ansible.runner.shell_plugins import powershell
|
|
||||||
|
|
||||||
try:
|
|
||||||
from winrm import Response
|
|
||||||
from winrm.exceptions import WinRMTransportError
|
|
||||||
from winrm.protocol import Protocol
|
|
||||||
except ImportError:
|
|
||||||
raise errors.AnsibleError("winrm is not installed")
|
|
||||||
|
|
||||||
HAVE_KERBEROS = False
|
|
||||||
try:
|
|
||||||
import kerberos
|
|
||||||
HAVE_KERBEROS = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def vvvvv(msg, host=None):
|
|
||||||
verbose(msg, host=host, caplevel=4)
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
'''WinRM connections over HTTP/HTTPS.'''
|
|
||||||
|
|
||||||
transport_schemes = {
|
|
||||||
'http': [('kerberos', 'http'), ('plaintext', 'http'), ('plaintext', 'https')],
|
|
||||||
'https': [('kerberos', 'https'), ('plaintext', 'https')],
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, user, password, *args, **kwargs):
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.user = user
|
|
||||||
self.password = password
|
|
||||||
self.has_pipelining = False
|
|
||||||
self.default_shell = 'powershell'
|
|
||||||
self.default_suffixes = ['.ps1', '']
|
|
||||||
self.protocol = None
|
|
||||||
self.shell_id = None
|
|
||||||
self.delegate = None
|
|
||||||
|
|
||||||
# Add runas support
|
|
||||||
#self.become_methods_supported=['runas']
|
|
||||||
self.become_methods_supported=[]
|
|
||||||
|
|
||||||
def _winrm_connect(self):
|
|
||||||
'''
|
|
||||||
Establish a WinRM connection over HTTP/HTTPS.
|
|
||||||
'''
|
|
||||||
port = self.port or 5986
|
|
||||||
vvv("ESTABLISH WINRM CONNECTION FOR USER: %s on PORT %s TO %s" % \
|
|
||||||
(self.user, port, self.host), host=self.host)
|
|
||||||
netloc = '%s:%d' % (self.host, port)
|
|
||||||
exc = None
|
|
||||||
for transport, scheme in self.transport_schemes['http' if port == 5985 else 'https']:
|
|
||||||
if transport == 'kerberos' and (not HAVE_KERBEROS or not '@' in self.user):
|
|
||||||
continue
|
|
||||||
if transport == 'kerberos':
|
|
||||||
realm = self.user.split('@', 1)[1].strip() or None
|
|
||||||
else:
|
|
||||||
realm = None
|
|
||||||
endpoint = urlparse.urlunsplit((scheme, netloc, '/wsman', '', ''))
|
|
||||||
vvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint),
|
|
||||||
host=self.host)
|
|
||||||
protocol = Protocol(endpoint, transport=transport,
|
|
||||||
username=self.user, password=self.password,
|
|
||||||
realm=realm)
|
|
||||||
try:
|
|
||||||
protocol.send_message('')
|
|
||||||
return protocol
|
|
||||||
except WinRMTransportError, exc:
|
|
||||||
err_msg = str(exc)
|
|
||||||
if re.search(r'Operation\s+?timed\s+?out', err_msg, re.I):
|
|
||||||
raise errors.AnsibleError("the connection attempt timed out")
|
|
||||||
m = re.search(r'Code\s+?(\d{3})', err_msg)
|
|
||||||
if m:
|
|
||||||
code = int(m.groups()[0])
|
|
||||||
if code == 401:
|
|
||||||
raise errors.AnsibleError("the username/password specified for this server was incorrect")
|
|
||||||
elif code == 411:
|
|
||||||
return protocol
|
|
||||||
vvvv('WINRM CONNECTION ERROR: %s' % err_msg, host=self.host)
|
|
||||||
continue
|
|
||||||
if exc:
|
|
||||||
raise errors.AnsibleError(str(exc))
|
|
||||||
|
|
||||||
def _winrm_exec(self, command, args=(), from_exec=False):
|
|
||||||
if from_exec:
|
|
||||||
vvvv("WINRM EXEC %r %r" % (command, args), host=self.host)
|
|
||||||
else:
|
|
||||||
vvvvv("WINRM EXEC %r %r" % (command, args), host=self.host)
|
|
||||||
if not self.protocol:
|
|
||||||
self.protocol = self._winrm_connect()
|
|
||||||
if not self.shell_id:
|
|
||||||
self.shell_id = self.protocol.open_shell()
|
|
||||||
command_id = None
|
|
||||||
try:
|
|
||||||
command_id = self.protocol.run_command(self.shell_id, command, args)
|
|
||||||
response = Response(self.protocol.get_command_output(self.shell_id, command_id))
|
|
||||||
if from_exec:
|
|
||||||
vvvv('WINRM RESULT %r' % response, host=self.host)
|
|
||||||
else:
|
|
||||||
vvvvv('WINRM RESULT %r' % response, host=self.host)
|
|
||||||
vvvvv('WINRM STDOUT %s' % response.std_out, host=self.host)
|
|
||||||
vvvvv('WINRM STDERR %s' % response.std_err, host=self.host)
|
|
||||||
return response
|
|
||||||
finally:
|
|
||||||
if command_id:
|
|
||||||
self.protocol.cleanup_command(self.shell_id, command_id)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
if not self.protocol:
|
|
||||||
self.protocol = self._winrm_connect()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable=None, in_data=None):
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
cmd = cmd.encode('utf-8')
|
|
||||||
cmd_parts = shlex.split(cmd, posix=False)
|
|
||||||
if '-EncodedCommand' in cmd_parts:
|
|
||||||
encoded_cmd = cmd_parts[cmd_parts.index('-EncodedCommand') + 1]
|
|
||||||
decoded_cmd = base64.b64decode(encoded_cmd)
|
|
||||||
vvv("EXEC %s" % decoded_cmd, host=self.host)
|
|
||||||
else:
|
|
||||||
vvv("EXEC %s" % cmd, host=self.host)
|
|
||||||
# For script/raw support.
|
|
||||||
if cmd_parts and cmd_parts[0].lower().endswith('.ps1'):
|
|
||||||
script = powershell._build_file_cmd(cmd_parts, quote_args=False)
|
|
||||||
cmd_parts = powershell._encode_script(script, as_list=True)
|
|
||||||
try:
|
|
||||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True)
|
|
||||||
except Exception, e:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to exec cmd %s" % cmd)
|
|
||||||
return (result.status_code, '', result.std_out.encode('utf-8'), result.std_err.encode('utf-8'))
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
with open(in_path) as in_file:
|
|
||||||
in_size = os.path.getsize(in_path)
|
|
||||||
script_template = '''
|
|
||||||
$s = [System.IO.File]::OpenWrite("%s");
|
|
||||||
[void]$s.Seek(%d, [System.IO.SeekOrigin]::Begin);
|
|
||||||
$b = [System.Convert]::FromBase64String("%s");
|
|
||||||
[void]$s.Write($b, 0, $b.length);
|
|
||||||
[void]$s.SetLength(%d);
|
|
||||||
[void]$s.Close();
|
|
||||||
'''
|
|
||||||
# Determine max size of data we can pass per command.
|
|
||||||
script = script_template % (powershell._escape(out_path), in_size, '', in_size)
|
|
||||||
cmd = powershell._encode_script(script)
|
|
||||||
# Encode script with no data, subtract its length from 8190 (max
|
|
||||||
# windows command length), divide by 2.67 (UTF16LE base64 command
|
|
||||||
# encoding), then by 1.35 again (data base64 encoding).
|
|
||||||
buffer_size = int(((8190 - len(cmd)) / 2.67) / 1.35)
|
|
||||||
for offset in xrange(0, in_size, buffer_size):
|
|
||||||
try:
|
|
||||||
out_data = in_file.read(buffer_size)
|
|
||||||
if offset == 0:
|
|
||||||
if out_data.lower().startswith('#!powershell') and not out_path.lower().endswith('.ps1'):
|
|
||||||
out_path = out_path + '.ps1'
|
|
||||||
b64_data = base64.b64encode(out_data)
|
|
||||||
script = script_template % (powershell._escape(out_path), offset, b64_data, in_size)
|
|
||||||
vvvv("WINRM PUT %s to %s (offset=%d size=%d)" % (in_path, out_path, offset, len(out_data)), host=self.host)
|
|
||||||
cmd_parts = powershell._encode_script(script, as_list=True)
|
|
||||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
|
||||||
if result.status_code != 0:
|
|
||||||
raise IOError(result.std_err.encode('utf-8'))
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
out_path = out_path.replace('\\', '/')
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
|
|
||||||
buffer_size = 2**19 # 0.5MB chunks
|
|
||||||
if not os.path.exists(os.path.dirname(out_path)):
|
|
||||||
os.makedirs(os.path.dirname(out_path))
|
|
||||||
out_file = None
|
|
||||||
try:
|
|
||||||
offset = 0
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
script = '''
|
|
||||||
If (Test-Path -PathType Leaf "%(path)s")
|
|
||||||
{
|
|
||||||
$stream = [System.IO.File]::OpenRead("%(path)s");
|
|
||||||
$stream.Seek(%(offset)d, [System.IO.SeekOrigin]::Begin) | Out-Null;
|
|
||||||
$buffer = New-Object Byte[] %(buffer_size)d;
|
|
||||||
$bytesRead = $stream.Read($buffer, 0, %(buffer_size)d);
|
|
||||||
$bytes = $buffer[0..($bytesRead-1)];
|
|
||||||
[System.Convert]::ToBase64String($bytes);
|
|
||||||
$stream.Close() | Out-Null;
|
|
||||||
}
|
|
||||||
ElseIf (Test-Path -PathType Container "%(path)s")
|
|
||||||
{
|
|
||||||
Write-Host "[DIR]";
|
|
||||||
}
|
|
||||||
Else
|
|
||||||
{
|
|
||||||
Write-Error "%(path)s does not exist";
|
|
||||||
Exit 1;
|
|
||||||
}
|
|
||||||
''' % dict(buffer_size=buffer_size, path=powershell._escape(in_path), offset=offset)
|
|
||||||
vvvv("WINRM FETCH %s to %s (offset=%d)" % (in_path, out_path, offset), host=self.host)
|
|
||||||
cmd_parts = powershell._encode_script(script, as_list=True)
|
|
||||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
|
||||||
if result.status_code != 0:
|
|
||||||
raise IOError(result.std_err.encode('utf-8'))
|
|
||||||
if result.std_out.strip() == '[DIR]':
|
|
||||||
data = None
|
|
||||||
else:
|
|
||||||
data = base64.b64decode(result.std_out.strip())
|
|
||||||
if data is None:
|
|
||||||
if not os.path.exists(out_path):
|
|
||||||
os.makedirs(out_path)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if not out_file:
|
|
||||||
# If out_path is a directory and we're expecting a file, bail out now.
|
|
||||||
if os.path.isdir(out_path):
|
|
||||||
break
|
|
||||||
out_file = open(out_path, 'wb')
|
|
||||||
out_file.write(data)
|
|
||||||
if len(data) < buffer_size:
|
|
||||||
break
|
|
||||||
offset += len(data)
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
finally:
|
|
||||||
if out_file:
|
|
||||||
out_file.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.protocol and self.shell_id:
|
|
||||||
self.protocol.close_shell(self.shell_id)
|
|
||||||
self.shell_id = None
|
|
@ -1,162 +0,0 @@
|
|||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
|
||||||
# and jail.py (c) 2013, Michael Scherer <misc@zarb.org>
|
|
||||||
# (c) 2015, Dagobert Michelsen <dam@baltic-online.de>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import distutils.spawn
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from subprocess import Popen,PIPE
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.callbacks import vvv
|
|
||||||
import ansible.constants as C
|
|
||||||
|
|
||||||
class Connection(object):
|
|
||||||
''' Local zone based connections '''
|
|
||||||
|
|
||||||
def _search_executable(self, executable):
|
|
||||||
cmd = distutils.spawn.find_executable(executable)
|
|
||||||
if not cmd:
|
|
||||||
raise errors.AnsibleError("%s command not found in PATH") % executable
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def list_zones(self):
|
|
||||||
pipe = subprocess.Popen([self.zoneadm_cmd, 'list', '-ip'],
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
#stdout, stderr = p.communicate()
|
|
||||||
zones = []
|
|
||||||
for l in pipe.stdout.readlines():
|
|
||||||
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
|
||||||
s = l.split(':')
|
|
||||||
if s[1] != 'global':
|
|
||||||
zones.append(s[1])
|
|
||||||
|
|
||||||
return zones
|
|
||||||
|
|
||||||
def get_zone_path(self):
|
|
||||||
#solaris10vm# zoneadm -z cswbuild list -p
|
|
||||||
#-:cswbuild:installed:/zones/cswbuild:479f3c4b-d0c6-e97b-cd04-fd58f2c0238e:native:shared
|
|
||||||
pipe = subprocess.Popen([self.zoneadm_cmd, '-z', self.zone, 'list', '-p'],
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
#stdout, stderr = p.communicate()
|
|
||||||
path = pipe.stdout.readlines()[0].split(':')[3]
|
|
||||||
return path + '/root'
|
|
||||||
|
|
||||||
def __init__(self, runner, host, port, *args, **kwargs):
|
|
||||||
self.zone = host
|
|
||||||
self.runner = runner
|
|
||||||
self.host = host
|
|
||||||
self.has_pipelining = False
|
|
||||||
self.become_methods_supported=C.BECOME_METHODS
|
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise errors.AnsibleError("zone connection requires running as root")
|
|
||||||
|
|
||||||
self.zoneadm_cmd = self._search_executable('zoneadm')
|
|
||||||
self.zlogin_cmd = self._search_executable('zlogin')
|
|
||||||
|
|
||||||
if not self.zone in self.list_zones():
|
|
||||||
raise errors.AnsibleError("incorrect zone name %s" % self.zone)
|
|
||||||
|
|
||||||
|
|
||||||
self.host = host
|
|
||||||
# port is unused, since this is local
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
def connect(self, port=None):
|
|
||||||
''' connect to the zone; nothing to do here '''
|
|
||||||
|
|
||||||
vvv("THIS IS A LOCAL ZONE DIR", host=self.zone)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
# a modifier
|
|
||||||
def _generate_cmd(self, executable, cmd):
|
|
||||||
if executable:
|
|
||||||
local_cmd = [self.zlogin_cmd, self.zone, executable, cmd]
|
|
||||||
else:
|
|
||||||
local_cmd = '%s "%s" %s' % (self.zlogin_cmd, self.zone, cmd)
|
|
||||||
return local_cmd
|
|
||||||
|
|
||||||
def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable=None, in_data=None):
|
|
||||||
''' run a command on the zone '''
|
|
||||||
|
|
||||||
if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
|
|
||||||
|
|
||||||
if in_data:
|
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
|
||||||
|
|
||||||
# We happily ignore privilege escalation
|
|
||||||
if executable == '/bin/sh':
|
|
||||||
executable = None
|
|
||||||
local_cmd = self._generate_cmd(executable, cmd)
|
|
||||||
|
|
||||||
vvv("EXEC %s" % (local_cmd), host=self.zone)
|
|
||||||
p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
|
|
||||||
cwd=self.runner.basedir,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
return (p.returncode, '', stdout, stderr)
|
|
||||||
|
|
||||||
def _normalize_path(self, path, prefix):
|
|
||||||
if not path.startswith(os.path.sep):
|
|
||||||
path = os.path.join(os.path.sep, path)
|
|
||||||
normpath = os.path.normpath(path)
|
|
||||||
return os.path.join(prefix, normpath[1:])
|
|
||||||
|
|
||||||
def _copy_file(self, in_path, out_path):
|
|
||||||
if not os.path.exists(in_path):
|
|
||||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
||||||
try:
|
|
||||||
shutil.copyfile(in_path, out_path)
|
|
||||||
except shutil.Error:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
|
|
||||||
except IOError:
|
|
||||||
traceback.print_exc()
|
|
||||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
|
||||||
''' transfer a file from local to zone '''
|
|
||||||
|
|
||||||
out_path = self._normalize_path(out_path, self.get_zone_path())
|
|
||||||
vvv("PUT %s TO %s" % (in_path, out_path), host=self.zone)
|
|
||||||
|
|
||||||
self._copy_file(in_path, out_path)
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
|
||||||
''' fetch a file from zone to local '''
|
|
||||||
|
|
||||||
in_path = self._normalize_path(in_path, self.get_zone_path())
|
|
||||||
vvv("FETCH %s TO %s" % (in_path, out_path), host=self.zone)
|
|
||||||
|
|
||||||
self._copy_file(in_path, out_path)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
''' terminate the connection; nothing to do here '''
|
|
||||||
pass
|
|
@ -1,431 +0,0 @@
|
|||||||
# (c) 2012, Jeroen Hoekx <jeroen@hoekx.be>
|
|
||||||
#
|
|
||||||
# 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 __future__ import absolute_import
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import os.path
|
|
||||||
import types
|
|
||||||
import pipes
|
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
import crypt
|
|
||||||
import hashlib
|
|
||||||
import string
|
|
||||||
from functools import partial
|
|
||||||
import operator as py_operator
|
|
||||||
from random import SystemRandom, shuffle
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from jinja2.filters import environmentfilter
|
|
||||||
from distutils.version import LooseVersion, StrictVersion
|
|
||||||
|
|
||||||
from ansible import errors
|
|
||||||
from ansible.utils.hashing import md5s, checksum_s
|
|
||||||
from ansible.utils.unicode import unicode_wrap, to_unicode
|
|
||||||
|
|
||||||
|
|
||||||
UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')
|
|
||||||
|
|
||||||
|
|
||||||
def to_nice_yaml(*a, **kw):
|
|
||||||
'''Make verbose, human readable yaml'''
|
|
||||||
transformed = yaml.safe_dump(*a, indent=4, allow_unicode=True, default_flow_style=False, **kw)
|
|
||||||
return to_unicode(transformed)
|
|
||||||
|
|
||||||
def to_json(a, *args, **kw):
|
|
||||||
''' Convert the value to JSON '''
|
|
||||||
return json.dumps(a, *args, **kw)
|
|
||||||
|
|
||||||
def to_nice_json(a, *args, **kw):
|
|
||||||
'''Make verbose, human readable JSON'''
|
|
||||||
# python-2.6's json encoder is buggy (can't encode hostvars)
|
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
try:
|
|
||||||
import simplejson
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
major = int(simplejson.__version__.split('.')[0])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if major >= 2:
|
|
||||||
return simplejson.dumps(a, indent=4, sort_keys=True, *args, **kw)
|
|
||||||
# Fallback to the to_json filter
|
|
||||||
return to_json(a, *args, **kw)
|
|
||||||
return json.dumps(a, indent=4, sort_keys=True, *args, **kw)
|
|
||||||
|
|
||||||
def failed(*a, **kw):
|
|
||||||
''' Test if task result yields failed '''
|
|
||||||
item = a[0]
|
|
||||||
if type(item) != dict:
|
|
||||||
raise errors.AnsibleFilterError("|failed expects a dictionary")
|
|
||||||
rc = item.get('rc',0)
|
|
||||||
failed = item.get('failed',False)
|
|
||||||
if rc != 0 or failed:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def success(*a, **kw):
|
|
||||||
''' Test if task result yields success '''
|
|
||||||
return not failed(*a, **kw)
|
|
||||||
|
|
||||||
def changed(*a, **kw):
|
|
||||||
''' Test if task result yields changed '''
|
|
||||||
item = a[0]
|
|
||||||
if type(item) != dict:
|
|
||||||
raise errors.AnsibleFilterError("|changed expects a dictionary")
|
|
||||||
if not 'changed' in item:
|
|
||||||
changed = False
|
|
||||||
if ('results' in item # some modules return a 'results' key
|
|
||||||
and type(item['results']) == list
|
|
||||||
and type(item['results'][0]) == dict):
|
|
||||||
for result in item['results']:
|
|
||||||
changed = changed or result.get('changed', False)
|
|
||||||
else:
|
|
||||||
changed = item.get('changed', False)
|
|
||||||
return changed
|
|
||||||
|
|
||||||
def skipped(*a, **kw):
|
|
||||||
''' Test if task result yields skipped '''
|
|
||||||
item = a[0]
|
|
||||||
if type(item) != dict:
|
|
||||||
raise errors.AnsibleFilterError("|skipped expects a dictionary")
|
|
||||||
skipped = item.get('skipped', False)
|
|
||||||
return skipped
|
|
||||||
|
|
||||||
def mandatory(a):
|
|
||||||
''' Make a variable mandatory '''
|
|
||||||
try:
|
|
||||||
a
|
|
||||||
except NameError:
|
|
||||||
raise errors.AnsibleFilterError('Mandatory variable not defined.')
|
|
||||||
else:
|
|
||||||
return a
|
|
||||||
|
|
||||||
def bool(a):
|
|
||||||
''' return a bool for the arg '''
|
|
||||||
if a is None or type(a) == bool:
|
|
||||||
return a
|
|
||||||
if type(a) in types.StringTypes:
|
|
||||||
a = a.lower()
|
|
||||||
if a in ['yes', 'on', '1', 'true', 1]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def quote(a):
|
|
||||||
''' return its argument quoted for shell usage '''
|
|
||||||
return pipes.quote(a)
|
|
||||||
|
|
||||||
def fileglob(pathname):
|
|
||||||
''' return list of matched files for glob '''
|
|
||||||
return glob.glob(pathname)
|
|
||||||
|
|
||||||
def regex(value='', pattern='', ignorecase=False, match_type='search'):
|
|
||||||
''' Expose `re` as a boolean filter using the `search` method by default.
|
|
||||||
This is likely only useful for `search` and `match` which already
|
|
||||||
have their own filters.
|
|
||||||
'''
|
|
||||||
if ignorecase:
|
|
||||||
flags = re.I
|
|
||||||
else:
|
|
||||||
flags = 0
|
|
||||||
_re = re.compile(pattern, flags=flags)
|
|
||||||
_bool = __builtins__.get('bool')
|
|
||||||
return _bool(getattr(_re, match_type, 'search')(value))
|
|
||||||
|
|
||||||
def match(value, pattern='', ignorecase=False):
|
|
||||||
''' Perform a `re.match` returning a boolean '''
|
|
||||||
return regex(value, pattern, ignorecase, 'match')
|
|
||||||
|
|
||||||
def search(value, pattern='', ignorecase=False):
|
|
||||||
''' Perform a `re.search` returning a boolean '''
|
|
||||||
return regex(value, pattern, ignorecase, 'search')
|
|
||||||
|
|
||||||
def regex_replace(value='', pattern='', replacement='', ignorecase=False):
|
|
||||||
''' Perform a `re.sub` returning a string '''
|
|
||||||
|
|
||||||
if not isinstance(value, basestring):
|
|
||||||
value = str(value)
|
|
||||||
|
|
||||||
if ignorecase:
|
|
||||||
flags = re.I
|
|
||||||
else:
|
|
||||||
flags = 0
|
|
||||||
_re = re.compile(pattern, flags=flags)
|
|
||||||
return _re.sub(replacement, value)
|
|
||||||
|
|
||||||
def ternary(value, true_val, false_val):
|
|
||||||
''' value ? true_val : false_val '''
|
|
||||||
if value:
|
|
||||||
return true_val
|
|
||||||
else:
|
|
||||||
return false_val
|
|
||||||
|
|
||||||
|
|
||||||
def version_compare(value, version, operator='eq', strict=False):
|
|
||||||
''' Perform a version comparison on a value '''
|
|
||||||
op_map = {
|
|
||||||
'==': 'eq', '=': 'eq', 'eq': 'eq',
|
|
||||||
'<': 'lt', 'lt': 'lt',
|
|
||||||
'<=': 'le', 'le': 'le',
|
|
||||||
'>': 'gt', 'gt': 'gt',
|
|
||||||
'>=': 'ge', 'ge': 'ge',
|
|
||||||
'!=': 'ne', '<>': 'ne', 'ne': 'ne'
|
|
||||||
}
|
|
||||||
|
|
||||||
if strict:
|
|
||||||
Version = StrictVersion
|
|
||||||
else:
|
|
||||||
Version = LooseVersion
|
|
||||||
|
|
||||||
if operator in op_map:
|
|
||||||
operator = op_map[operator]
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleFilterError('Invalid operator type')
|
|
||||||
|
|
||||||
try:
|
|
||||||
method = getattr(py_operator, operator)
|
|
||||||
return method(Version(str(value)), Version(str(version)))
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleFilterError('Version comparison: %s' % e)
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def rand(environment, end, start=None, step=None):
|
|
||||||
r = SystemRandom()
|
|
||||||
if isinstance(end, (int, long)):
|
|
||||||
if not start:
|
|
||||||
start = 0
|
|
||||||
if not step:
|
|
||||||
step = 1
|
|
||||||
return r.randrange(start, end, step)
|
|
||||||
elif hasattr(end, '__iter__'):
|
|
||||||
if start or step:
|
|
||||||
raise errors.AnsibleFilterError('start and step can only be used with integer values')
|
|
||||||
return r.choice(end)
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleFilterError('random can only be used on sequences and integers')
|
|
||||||
|
|
||||||
def randomize_list(mylist):
|
|
||||||
try:
|
|
||||||
mylist = list(mylist)
|
|
||||||
shuffle(mylist)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return mylist
|
|
||||||
|
|
||||||
def get_hash(data, hashtype='sha1'):
|
|
||||||
|
|
||||||
try: # see if hash is supported
|
|
||||||
h = hashlib.new(hashtype)
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
h.update(data)
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
def get_encrypted_password(password, hashtype='sha512', salt=None):
|
|
||||||
|
|
||||||
# TODO: find a way to construct dynamically from system
|
|
||||||
cryptmethod= {
|
|
||||||
'md5': '1',
|
|
||||||
'blowfish': '2a',
|
|
||||||
'sha256': '5',
|
|
||||||
'sha512': '6',
|
|
||||||
}
|
|
||||||
|
|
||||||
hastype = hashtype.lower()
|
|
||||||
if hashtype in cryptmethod:
|
|
||||||
if salt is None:
|
|
||||||
r = SystemRandom()
|
|
||||||
salt = ''.join([r.choice(string.ascii_letters + string.digits) for _ in range(16)])
|
|
||||||
|
|
||||||
saltstring = "$%s$%s" % (cryptmethod[hashtype],salt)
|
|
||||||
encrypted = crypt.crypt(password,saltstring)
|
|
||||||
return encrypted
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def to_uuid(string):
|
|
||||||
return str(uuid.uuid5(UUID_NAMESPACE_ANSIBLE, str(string)))
|
|
||||||
|
|
||||||
def comment(text, style='plain', **kw):
|
|
||||||
# Predefined comment types
|
|
||||||
comment_styles = {
|
|
||||||
'plain': {
|
|
||||||
'decoration': '# '
|
|
||||||
},
|
|
||||||
'erlang': {
|
|
||||||
'decoration': '% '
|
|
||||||
},
|
|
||||||
'c': {
|
|
||||||
'decoration': '// '
|
|
||||||
},
|
|
||||||
'cblock': {
|
|
||||||
'beginning': '/*',
|
|
||||||
'decoration': ' * ',
|
|
||||||
'end': ' */'
|
|
||||||
},
|
|
||||||
'xml': {
|
|
||||||
'beginning': '<!--',
|
|
||||||
'decoration': ' - ',
|
|
||||||
'end': '-->'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pointer to the right comment type
|
|
||||||
style_params = comment_styles[style]
|
|
||||||
|
|
||||||
if 'decoration' in kw:
|
|
||||||
prepostfix = kw['decoration']
|
|
||||||
else:
|
|
||||||
prepostfix = style_params['decoration']
|
|
||||||
|
|
||||||
# Default params
|
|
||||||
p = {
|
|
||||||
'newline': '\n',
|
|
||||||
'beginning': '',
|
|
||||||
'prefix': (prepostfix).rstrip(),
|
|
||||||
'prefix_count': 1,
|
|
||||||
'decoration': '',
|
|
||||||
'postfix': (prepostfix).rstrip(),
|
|
||||||
'postfix_count': 1,
|
|
||||||
'end': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update default params
|
|
||||||
p.update(style_params)
|
|
||||||
p.update(kw)
|
|
||||||
|
|
||||||
# Compose substrings for the final string
|
|
||||||
str_beginning = ''
|
|
||||||
if p['beginning']:
|
|
||||||
str_beginning = "%s%s" % (p['beginning'], p['newline'])
|
|
||||||
str_prefix = str(
|
|
||||||
"%s%s" % (p['prefix'], p['newline'])) * int(p['prefix_count'])
|
|
||||||
str_text = ("%s%s" % (
|
|
||||||
p['decoration'],
|
|
||||||
# Prepend each line of the text with the decorator
|
|
||||||
text.replace(
|
|
||||||
p['newline'], "%s%s" % (p['newline'], p['decoration'])))).replace(
|
|
||||||
# Remove trailing spaces when only decorator is on the line
|
|
||||||
"%s%s" % (p['decoration'], p['newline']),
|
|
||||||
"%s%s" % (p['decoration'].rstrip(), p['newline']))
|
|
||||||
str_postfix = p['newline'].join(
|
|
||||||
[''] + [p['postfix'] for x in range(p['postfix_count'])])
|
|
||||||
str_end = ''
|
|
||||||
if p['end']:
|
|
||||||
str_end = "%s%s" % (p['newline'], p['end'])
|
|
||||||
|
|
||||||
# Return the final string
|
|
||||||
return "%s%s%s%s%s" % (
|
|
||||||
str_beginning,
|
|
||||||
str_prefix,
|
|
||||||
str_text,
|
|
||||||
str_postfix,
|
|
||||||
str_end)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterModule(object):
|
|
||||||
''' Ansible core jinja2 filters '''
|
|
||||||
|
|
||||||
def filters(self):
|
|
||||||
return {
|
|
||||||
# base 64
|
|
||||||
'b64decode': partial(unicode_wrap, base64.b64decode),
|
|
||||||
'b64encode': partial(unicode_wrap, base64.b64encode),
|
|
||||||
|
|
||||||
# uuid
|
|
||||||
'to_uuid': to_uuid,
|
|
||||||
|
|
||||||
# json
|
|
||||||
'to_json': to_json,
|
|
||||||
'to_nice_json': to_nice_json,
|
|
||||||
'from_json': json.loads,
|
|
||||||
|
|
||||||
# yaml
|
|
||||||
'to_yaml': yaml.safe_dump,
|
|
||||||
'to_nice_yaml': to_nice_yaml,
|
|
||||||
'from_yaml': yaml.safe_load,
|
|
||||||
|
|
||||||
# path
|
|
||||||
'basename': partial(unicode_wrap, os.path.basename),
|
|
||||||
'dirname': partial(unicode_wrap, os.path.dirname),
|
|
||||||
'expanduser': partial(unicode_wrap, os.path.expanduser),
|
|
||||||
'realpath': partial(unicode_wrap, os.path.realpath),
|
|
||||||
'relpath': partial(unicode_wrap, os.path.relpath),
|
|
||||||
|
|
||||||
# failure testing
|
|
||||||
'failed' : failed,
|
|
||||||
'success' : success,
|
|
||||||
|
|
||||||
# changed testing
|
|
||||||
'changed' : changed,
|
|
||||||
|
|
||||||
# skip testing
|
|
||||||
'skipped' : skipped,
|
|
||||||
|
|
||||||
# variable existence
|
|
||||||
'mandatory': mandatory,
|
|
||||||
|
|
||||||
# value as boolean
|
|
||||||
'bool': bool,
|
|
||||||
|
|
||||||
# quote string for shell usage
|
|
||||||
'quote': quote,
|
|
||||||
|
|
||||||
# hash filters
|
|
||||||
# md5 hex digest of string
|
|
||||||
'md5': md5s,
|
|
||||||
# sha1 hex digeset of string
|
|
||||||
'sha1': checksum_s,
|
|
||||||
# checksum of string as used by ansible for checksuming files
|
|
||||||
'checksum': checksum_s,
|
|
||||||
# generic hashing
|
|
||||||
'password_hash': get_encrypted_password,
|
|
||||||
'hash': get_hash,
|
|
||||||
|
|
||||||
# file glob
|
|
||||||
'fileglob': fileglob,
|
|
||||||
|
|
||||||
# regex
|
|
||||||
'match': match,
|
|
||||||
'search': search,
|
|
||||||
'regex': regex,
|
|
||||||
'regex_replace': regex_replace,
|
|
||||||
|
|
||||||
# ? : ;
|
|
||||||
'ternary': ternary,
|
|
||||||
|
|
||||||
# list
|
|
||||||
# version comparison
|
|
||||||
'version_compare': version_compare,
|
|
||||||
|
|
||||||
# random stuff
|
|
||||||
'random': rand,
|
|
||||||
'shuffle': randomize_list,
|
|
||||||
|
|
||||||
# comment-style decoration of string
|
|
||||||
'comment': comment,
|
|
||||||
}
|
|
@ -1,659 +0,0 @@
|
|||||||
# (c) 2014, Maciej Delmanowski <drybjed@gmail.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 functools import partial
|
|
||||||
|
|
||||||
try:
|
|
||||||
import netaddr
|
|
||||||
except ImportError:
|
|
||||||
# in this case, we'll make the filters return error messages (see bottom)
|
|
||||||
netaddr = None
|
|
||||||
else:
|
|
||||||
class mac_linux(netaddr.mac_unix):
|
|
||||||
pass
|
|
||||||
mac_linux.word_fmt = '%.2x'
|
|
||||||
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
|
|
||||||
# ---- IP address and network query helpers ----
|
|
||||||
|
|
||||||
def _empty_ipaddr_query(v, vtype):
|
|
||||||
# We don't have any query to process, so just check what type the user
|
|
||||||
# expects, and return the IP address in a correct format
|
|
||||||
if v:
|
|
||||||
if vtype == 'address':
|
|
||||||
return str(v.ip)
|
|
||||||
elif vtype == 'network':
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _6to4_query(v, vtype, value):
|
|
||||||
if v.version == 4:
|
|
||||||
|
|
||||||
if v.size == 1:
|
|
||||||
ipconv = str(v.ip)
|
|
||||||
elif v.size > 1:
|
|
||||||
if v.ip != v.network:
|
|
||||||
ipconv = str(v.ip)
|
|
||||||
else:
|
|
||||||
ipconv = False
|
|
||||||
|
|
||||||
if ipaddr(ipconv, 'public'):
|
|
||||||
numbers = list(map(int, ipconv.split('.')))
|
|
||||||
|
|
||||||
try:
|
|
||||||
return '2002:{:02x}{:02x}:{:02x}{:02x}::1/48'.format(*numbers)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
elif v.version == 6:
|
|
||||||
if vtype == 'address':
|
|
||||||
if ipaddr(str(v), '2002::/16'):
|
|
||||||
return value
|
|
||||||
elif vtype == 'network':
|
|
||||||
if v.ip != v.network:
|
|
||||||
if ipaddr(str(v.ip), '2002::/16'):
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _ip_query(v):
|
|
||||||
if v.size == 1:
|
|
||||||
return str(v.ip)
|
|
||||||
if v.size > 1:
|
|
||||||
if v.ip != v.network:
|
|
||||||
return str(v.ip)
|
|
||||||
|
|
||||||
def _gateway_query(v):
|
|
||||||
if v.size > 1:
|
|
||||||
if v.ip != v.network:
|
|
||||||
return str(v.ip) + '/' + str(v.prefixlen)
|
|
||||||
|
|
||||||
def _bool_ipaddr_query(v):
|
|
||||||
if v:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _broadcast_query(v):
|
|
||||||
if v.size > 1:
|
|
||||||
return str(v.broadcast)
|
|
||||||
|
|
||||||
def _cidr_query(v):
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _cidr_lookup_query(v, iplist, value):
|
|
||||||
try:
|
|
||||||
if v in iplist:
|
|
||||||
return value
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _host_query(v):
|
|
||||||
if v.size == 1:
|
|
||||||
return str(v)
|
|
||||||
elif v.size > 1:
|
|
||||||
if v.ip != v.network:
|
|
||||||
return str(v.ip) + '/' + str(v.prefixlen)
|
|
||||||
|
|
||||||
def _hostmask_query(v):
|
|
||||||
return str(v.hostmask)
|
|
||||||
|
|
||||||
def _int_query(v, vtype):
|
|
||||||
if vtype == 'address':
|
|
||||||
return int(v.ip)
|
|
||||||
elif vtype == 'network':
|
|
||||||
return str(int(v.ip)) + '/' + str(int(v.prefixlen))
|
|
||||||
|
|
||||||
def _ipv4_query(v, value):
|
|
||||||
if v.version == 6:
|
|
||||||
try:
|
|
||||||
return str(v.ipv4())
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _ipv6_query(v, value):
|
|
||||||
if v.version == 4:
|
|
||||||
return str(v.ipv6())
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _link_local_query(v, value):
|
|
||||||
v_ip = netaddr.IPAddress(str(v.ip))
|
|
||||||
if v.version == 4:
|
|
||||||
if ipaddr(str(v_ip), '169.254.0.0/24'):
|
|
||||||
return value
|
|
||||||
|
|
||||||
elif v.version == 6:
|
|
||||||
if ipaddr(str(v_ip), 'fe80::/10'):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _loopback_query(v, value):
|
|
||||||
v_ip = netaddr.IPAddress(str(v.ip))
|
|
||||||
if v_ip.is_loopback():
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _multicast_query(v, value):
|
|
||||||
if v.is_multicast():
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _net_query(v):
|
|
||||||
if v.size > 1:
|
|
||||||
if v.ip == v.network:
|
|
||||||
return str(v.network) + '/' + str(v.prefixlen)
|
|
||||||
|
|
||||||
def _netmask_query(v):
|
|
||||||
if v.size > 1:
|
|
||||||
return str(v.netmask)
|
|
||||||
|
|
||||||
def _network_query(v):
|
|
||||||
if v.size > 1:
|
|
||||||
return str(v.network)
|
|
||||||
|
|
||||||
def _prefix_query(v):
|
|
||||||
return int(v.prefixlen)
|
|
||||||
|
|
||||||
def _private_query(v, value):
|
|
||||||
if v.is_private():
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _public_query(v, value):
|
|
||||||
v_ip = netaddr.IPAddress(str(v.ip))
|
|
||||||
if v_ip.is_unicast() and not v_ip.is_private() and \
|
|
||||||
not v_ip.is_loopback() and not v_ip.is_netmask() and \
|
|
||||||
not v_ip.is_hostmask():
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _revdns_query(v):
|
|
||||||
v_ip = netaddr.IPAddress(str(v.ip))
|
|
||||||
return v_ip.reverse_dns
|
|
||||||
|
|
||||||
def _size_query(v):
|
|
||||||
return v.size
|
|
||||||
|
|
||||||
def _subnet_query(v):
|
|
||||||
return str(v.cidr)
|
|
||||||
|
|
||||||
def _type_query(v):
|
|
||||||
if v.size == 1:
|
|
||||||
return 'address'
|
|
||||||
if v.size > 1:
|
|
||||||
if v.ip != v.network:
|
|
||||||
return 'address'
|
|
||||||
else:
|
|
||||||
return 'network'
|
|
||||||
|
|
||||||
def _unicast_query(v, value):
|
|
||||||
if v.is_unicast():
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _version_query(v):
|
|
||||||
return v.version
|
|
||||||
|
|
||||||
def _wrap_query(v, vtype, value):
|
|
||||||
if v.version == 6:
|
|
||||||
if vtype == 'address':
|
|
||||||
return '[' + str(v.ip) + ']'
|
|
||||||
elif vtype == 'network':
|
|
||||||
return '[' + str(v.ip) + ']/' + str(v.prefixlen)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
# ---- HWaddr query helpers ----
|
|
||||||
def _bare_query(v):
|
|
||||||
v.dialect = netaddr.mac_bare
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _bool_hwaddr_query(v):
|
|
||||||
if v:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _cisco_query(v):
|
|
||||||
v.dialect = netaddr.mac_cisco
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _empty_hwaddr_query(v, value):
|
|
||||||
if v:
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _linux_query(v):
|
|
||||||
v.dialect = mac_linux
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _postgresql_query(v):
|
|
||||||
v.dialect = netaddr.mac_pgsql
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _unix_query(v):
|
|
||||||
v.dialect = netaddr.mac_unix
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
def _win_query(v):
|
|
||||||
v.dialect = netaddr.mac_eui48
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- IP address and network filters ----
|
|
||||||
|
|
||||||
def ipaddr(value, query = '', version = False, alias = 'ipaddr'):
|
|
||||||
''' Check if string is an IP address or network and filter it '''
|
|
||||||
|
|
||||||
query_func_extra_args = {
|
|
||||||
'': ('vtype',),
|
|
||||||
'6to4': ('vtype', 'value'),
|
|
||||||
'cidr_lookup': ('iplist', 'value'),
|
|
||||||
'int': ('vtype',),
|
|
||||||
'ipv4': ('value',),
|
|
||||||
'ipv6': ('value',),
|
|
||||||
'link-local': ('value',),
|
|
||||||
'loopback': ('value',),
|
|
||||||
'lo': ('value',),
|
|
||||||
'multicast': ('value',),
|
|
||||||
'private': ('value',),
|
|
||||||
'public': ('value',),
|
|
||||||
'unicast': ('value',),
|
|
||||||
'wrap': ('vtype', 'value'),
|
|
||||||
}
|
|
||||||
query_func_map = {
|
|
||||||
'': _empty_ipaddr_query,
|
|
||||||
'6to4': _6to4_query,
|
|
||||||
'address': _ip_query,
|
|
||||||
'address/prefix': _gateway_query,
|
|
||||||
'bool': _bool_ipaddr_query,
|
|
||||||
'broadcast': _broadcast_query,
|
|
||||||
'cidr': _cidr_query,
|
|
||||||
'cidr_lookup': _cidr_lookup_query,
|
|
||||||
'gateway': _gateway_query,
|
|
||||||
'gw': _gateway_query,
|
|
||||||
'host': _host_query,
|
|
||||||
'host/prefix': _gateway_query,
|
|
||||||
'hostmask': _hostmask_query,
|
|
||||||
'hostnet': _gateway_query,
|
|
||||||
'int': _int_query,
|
|
||||||
'ip': _ip_query,
|
|
||||||
'ipv4': _ipv4_query,
|
|
||||||
'ipv6': _ipv6_query,
|
|
||||||
'link-local': _link_local_query,
|
|
||||||
'lo': _loopback_query,
|
|
||||||
'loopback': _loopback_query,
|
|
||||||
'multicast': _multicast_query,
|
|
||||||
'net': _net_query,
|
|
||||||
'netmask': _netmask_query,
|
|
||||||
'network': _network_query,
|
|
||||||
'prefix': _prefix_query,
|
|
||||||
'private': _private_query,
|
|
||||||
'public': _public_query,
|
|
||||||
'revdns': _revdns_query,
|
|
||||||
'router': _gateway_query,
|
|
||||||
'size': _size_query,
|
|
||||||
'subnet': _subnet_query,
|
|
||||||
'type': _type_query,
|
|
||||||
'unicast': _unicast_query,
|
|
||||||
'v4': _ipv4_query,
|
|
||||||
'v6': _ipv6_query,
|
|
||||||
'version': _version_query,
|
|
||||||
'wrap': _wrap_query,
|
|
||||||
}
|
|
||||||
|
|
||||||
vtype = None
|
|
||||||
|
|
||||||
if not value:
|
|
||||||
return False
|
|
||||||
|
|
||||||
elif value == True:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check if value is a list and parse each element
|
|
||||||
elif isinstance(value, (list, tuple)):
|
|
||||||
|
|
||||||
_ret = []
|
|
||||||
for element in value:
|
|
||||||
if ipaddr(element, str(query), version):
|
|
||||||
_ret.append(ipaddr(element, str(query), version))
|
|
||||||
|
|
||||||
if _ret:
|
|
||||||
return _ret
|
|
||||||
else:
|
|
||||||
return list()
|
|
||||||
|
|
||||||
# Check if value is a number and convert it to an IP address
|
|
||||||
elif str(value).isdigit():
|
|
||||||
|
|
||||||
# We don't know what IP version to assume, so let's check IPv4 first,
|
|
||||||
# then IPv6
|
|
||||||
try:
|
|
||||||
if ((not version) or (version and version == 4)):
|
|
||||||
v = netaddr.IPNetwork('0.0.0.0/0')
|
|
||||||
v.value = int(value)
|
|
||||||
v.prefixlen = 32
|
|
||||||
elif version and version == 6:
|
|
||||||
v = netaddr.IPNetwork('::/0')
|
|
||||||
v.value = int(value)
|
|
||||||
v.prefixlen = 128
|
|
||||||
|
|
||||||
# IPv4 didn't work the first time, so it definitely has to be IPv6
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
v = netaddr.IPNetwork('::/0')
|
|
||||||
v.value = int(value)
|
|
||||||
v.prefixlen = 128
|
|
||||||
|
|
||||||
# The value is too big for IPv6. Are you a nanobot?
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# We got an IP address, let's mark it as such
|
|
||||||
value = str(v)
|
|
||||||
vtype = 'address'
|
|
||||||
|
|
||||||
# value has not been recognized, check if it's a valid IP string
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
v = netaddr.IPNetwork(value)
|
|
||||||
|
|
||||||
# value is a valid IP string, check if user specified
|
|
||||||
# CIDR prefix or just an IP address, this will indicate default
|
|
||||||
# output format
|
|
||||||
try:
|
|
||||||
address, prefix = value.split('/')
|
|
||||||
vtype = 'network'
|
|
||||||
except:
|
|
||||||
vtype = 'address'
|
|
||||||
|
|
||||||
# value hasn't been recognized, maybe it's a numerical CIDR?
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
address, prefix = value.split('/')
|
|
||||||
address.isdigit()
|
|
||||||
address = int(address)
|
|
||||||
prefix.isdigit()
|
|
||||||
prefix = int(prefix)
|
|
||||||
|
|
||||||
# It's not numerical CIDR, give up
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# It is something, so let's try and build a CIDR from the parts
|
|
||||||
try:
|
|
||||||
v = netaddr.IPNetwork('0.0.0.0/0')
|
|
||||||
v.value = address
|
|
||||||
v.prefixlen = prefix
|
|
||||||
|
|
||||||
# It's not a valid IPv4 CIDR
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
v = netaddr.IPNetwork('::/0')
|
|
||||||
v.value = address
|
|
||||||
v.prefixlen = prefix
|
|
||||||
|
|
||||||
# It's not a valid IPv6 CIDR. Give up.
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# We have a valid CIDR, so let's write it in correct format
|
|
||||||
value = str(v)
|
|
||||||
vtype = 'network'
|
|
||||||
|
|
||||||
# We have a query string but it's not in the known query types. Check if
|
|
||||||
# that string is a valid subnet, if so, we can check later if given IP
|
|
||||||
# address/network is inside that specific subnet
|
|
||||||
try:
|
|
||||||
### ?? 6to4 and link-local were True here before. Should they still?
|
|
||||||
if query and (query not in query_func_map or query == 'cidr_lookup') and ipaddr(query, 'network'):
|
|
||||||
iplist = netaddr.IPSet([netaddr.IPNetwork(query)])
|
|
||||||
query = 'cidr_lookup'
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# This code checks if value maches the IP version the user wants, ie. if
|
|
||||||
# it's any version ("ipaddr()"), IPv4 ("ipv4()") or IPv6 ("ipv6()")
|
|
||||||
# If version does not match, return False
|
|
||||||
if version and v.version != version:
|
|
||||||
return False
|
|
||||||
|
|
||||||
extras = []
|
|
||||||
for arg in query_func_extra_args.get(query, tuple()):
|
|
||||||
extras.append(locals()[arg])
|
|
||||||
try:
|
|
||||||
return query_func_map[query](v, *extras)
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
float(query)
|
|
||||||
if v.size == 1:
|
|
||||||
if vtype == 'address':
|
|
||||||
return str(v.ip)
|
|
||||||
elif vtype == 'network':
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
elif v.size > 1:
|
|
||||||
try:
|
|
||||||
return str(v[query]) + '/' + str(v.prefixlen)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
except:
|
|
||||||
raise errors.AnsibleFilterError(alias + ': unknown filter type: %s' % query)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def ipwrap(value, query = ''):
|
|
||||||
try:
|
|
||||||
if isinstance(value, (list, tuple)):
|
|
||||||
_ret = []
|
|
||||||
for element in value:
|
|
||||||
if ipaddr(element, query, version = False, alias = 'ipwrap'):
|
|
||||||
_ret.append(ipaddr(element, 'wrap'))
|
|
||||||
else:
|
|
||||||
_ret.append(element)
|
|
||||||
|
|
||||||
return _ret
|
|
||||||
else:
|
|
||||||
_ret = ipaddr(value, query, version = False, alias = 'ipwrap')
|
|
||||||
if _ret:
|
|
||||||
return ipaddr(_ret, 'wrap')
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
except:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def ipv4(value, query = ''):
|
|
||||||
return ipaddr(value, query, version = 4, alias = 'ipv4')
|
|
||||||
|
|
||||||
|
|
||||||
def ipv6(value, query = ''):
|
|
||||||
return ipaddr(value, query, version = 6, alias = 'ipv6')
|
|
||||||
|
|
||||||
|
|
||||||
# Split given subnet into smaller subnets or find out the biggest subnet of
|
|
||||||
# a given IP address with given CIDR prefix
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# - address or address/prefix | ipsubnet
|
|
||||||
# returns CIDR subnet of a given input
|
|
||||||
#
|
|
||||||
# - address/prefix | ipsubnet(cidr)
|
|
||||||
# returns number of possible subnets for given CIDR prefix
|
|
||||||
#
|
|
||||||
# - address/prefix | ipsubnet(cidr, index)
|
|
||||||
# returns new subnet with given CIDR prefix
|
|
||||||
#
|
|
||||||
# - address | ipsubnet(cidr)
|
|
||||||
# returns biggest subnet with given CIDR prefix that address belongs to
|
|
||||||
#
|
|
||||||
# - address | ipsubnet(cidr, index)
|
|
||||||
# returns next indexed subnet which contains given address
|
|
||||||
def ipsubnet(value, query = '', index = 'x'):
|
|
||||||
''' Manipulate IPv4/IPv6 subnets '''
|
|
||||||
|
|
||||||
try:
|
|
||||||
vtype = ipaddr(value, 'type')
|
|
||||||
if vtype == 'address':
|
|
||||||
v = ipaddr(value, 'cidr')
|
|
||||||
elif vtype == 'network':
|
|
||||||
v = ipaddr(value, 'subnet')
|
|
||||||
|
|
||||||
value = netaddr.IPNetwork(v)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not query:
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
elif str(query).isdigit():
|
|
||||||
vsize = ipaddr(v, 'size')
|
|
||||||
query = int(query)
|
|
||||||
|
|
||||||
try:
|
|
||||||
float(index)
|
|
||||||
index = int(index)
|
|
||||||
|
|
||||||
if vsize > 1:
|
|
||||||
try:
|
|
||||||
return str(list(value.subnet(query))[index])
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
elif vsize == 1:
|
|
||||||
try:
|
|
||||||
return str(value.supernet(query)[index])
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
except:
|
|
||||||
if vsize > 1:
|
|
||||||
try:
|
|
||||||
return str(len(list(value.subnet(query))))
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
elif vsize == 1:
|
|
||||||
try:
|
|
||||||
return str(value.supernet(query)[0])
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Returns the nth host within a network described by value.
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# - address or address/prefix | nthhost(nth)
|
|
||||||
# returns the nth host within the given network
|
|
||||||
def nthhost(value, query=''):
|
|
||||||
''' Get the nth host within a given network '''
|
|
||||||
try:
|
|
||||||
vtype = ipaddr(value, 'type')
|
|
||||||
if vtype == 'address':
|
|
||||||
v = ipaddr(value, 'cidr')
|
|
||||||
elif vtype == 'network':
|
|
||||||
v = ipaddr(value, 'subnet')
|
|
||||||
|
|
||||||
value = netaddr.IPNetwork(v)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not query:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
vsize = ipaddr(v, 'size')
|
|
||||||
nth = int(query)
|
|
||||||
if value.size > nth:
|
|
||||||
return value[nth]
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# ---- HWaddr / MAC address filters ----
|
|
||||||
|
|
||||||
def hwaddr(value, query = '', alias = 'hwaddr'):
|
|
||||||
''' Check if string is a HW/MAC address and filter it '''
|
|
||||||
|
|
||||||
query_func_extra_args = {
|
|
||||||
'': ('value',),
|
|
||||||
}
|
|
||||||
query_func_map = {
|
|
||||||
'': _empty_hwaddr_query,
|
|
||||||
'bare': _bare_query,
|
|
||||||
'bool': _bool_hwaddr_query,
|
|
||||||
'cisco': _cisco_query,
|
|
||||||
'eui48': _win_query,
|
|
||||||
'linux': _linux_query,
|
|
||||||
'pgsql': _postgresql_query,
|
|
||||||
'postgresql': _postgresql_query,
|
|
||||||
'psql': _postgresql_query,
|
|
||||||
'unix': _unix_query,
|
|
||||||
'win': _win_query,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
v = netaddr.EUI(value)
|
|
||||||
except:
|
|
||||||
if query and query != 'bool':
|
|
||||||
raise errors.AnsibleFilterError(alias + ': not a hardware address: %s' % value)
|
|
||||||
|
|
||||||
extras = []
|
|
||||||
for arg in query_func_extra_args.get(query, tuple()):
|
|
||||||
extras.append(locals()[arg])
|
|
||||||
try:
|
|
||||||
return query_func_map[query](v, *extras)
|
|
||||||
except KeyError:
|
|
||||||
raise errors.AnsibleFilterError(alias + ': unknown filter type: %s' % query)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def macaddr(value, query = ''):
|
|
||||||
return hwaddr(value, query, alias = 'macaddr')
|
|
||||||
|
|
||||||
def _need_netaddr(f_name, *args, **kwargs):
|
|
||||||
raise errors.AnsibleFilterError('The {0} filter requires python-netaddr be'
|
|
||||||
' installed on the ansible controller'.format(f_name))
|
|
||||||
|
|
||||||
# ---- Ansible filters ----
|
|
||||||
|
|
||||||
class FilterModule(object):
|
|
||||||
''' IP address and network manipulation filters '''
|
|
||||||
filter_map = {
|
|
||||||
# IP addresses and networks
|
|
||||||
'ipaddr': ipaddr,
|
|
||||||
'ipwrap': ipwrap,
|
|
||||||
'ipv4': ipv4,
|
|
||||||
'ipv6': ipv6,
|
|
||||||
'ipsubnet': ipsubnet,
|
|
||||||
'nthhost': nthhost,
|
|
||||||
|
|
||||||
# MAC / HW addresses
|
|
||||||
'hwaddr': hwaddr,
|
|
||||||
'macaddr': macaddr
|
|
||||||
}
|
|
||||||
|
|
||||||
def filters(self):
|
|
||||||
if netaddr:
|
|
||||||
return self.filter_map
|
|
||||||
else:
|
|
||||||
# Need to install python-netaddr for these filters to work
|
|
||||||
return dict((f, partial(_need_netaddr, f)) for f in self.filter_map)
|
|
@ -1,126 +0,0 @@
|
|||||||
# (c) 2014, Brian Coca <bcoca@ansible.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 __future__ import absolute_import
|
|
||||||
|
|
||||||
import math
|
|
||||||
import collections
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
def unique(a):
|
|
||||||
if isinstance(a,collections.Hashable):
|
|
||||||
c = set(a)
|
|
||||||
else:
|
|
||||||
c = []
|
|
||||||
for x in a:
|
|
||||||
if x not in c:
|
|
||||||
c.append(x)
|
|
||||||
return c
|
|
||||||
|
|
||||||
def intersect(a, b):
|
|
||||||
if isinstance(a,collections.Hashable) and isinstance(b,collections.Hashable):
|
|
||||||
c = set(a) & set(b)
|
|
||||||
else:
|
|
||||||
c = unique(filter(lambda x: x in b, a))
|
|
||||||
return c
|
|
||||||
|
|
||||||
def difference(a, b):
|
|
||||||
if isinstance(a,collections.Hashable) and isinstance(b,collections.Hashable):
|
|
||||||
c = set(a) - set(b)
|
|
||||||
else:
|
|
||||||
c = unique(filter(lambda x: x not in b, a))
|
|
||||||
return c
|
|
||||||
|
|
||||||
def symmetric_difference(a, b):
|
|
||||||
if isinstance(a,collections.Hashable) and isinstance(b,collections.Hashable):
|
|
||||||
c = set(a) ^ set(b)
|
|
||||||
else:
|
|
||||||
c = unique(filter(lambda x: x not in intersect(a,b), union(a,b)))
|
|
||||||
return c
|
|
||||||
|
|
||||||
def union(a, b):
|
|
||||||
if isinstance(a,collections.Hashable) and isinstance(b,collections.Hashable):
|
|
||||||
c = set(a) | set(b)
|
|
||||||
else:
|
|
||||||
c = unique(a + b)
|
|
||||||
return c
|
|
||||||
|
|
||||||
def min(a):
|
|
||||||
_min = __builtins__.get('min')
|
|
||||||
return _min(a);
|
|
||||||
|
|
||||||
def max(a):
|
|
||||||
_max = __builtins__.get('max')
|
|
||||||
return _max(a);
|
|
||||||
|
|
||||||
def isnotanumber(x):
|
|
||||||
try:
|
|
||||||
return math.isnan(x)
|
|
||||||
except TypeError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def logarithm(x, base=math.e):
|
|
||||||
try:
|
|
||||||
if base == 10:
|
|
||||||
return math.log10(x)
|
|
||||||
else:
|
|
||||||
return math.log(x, base)
|
|
||||||
except TypeError, e:
|
|
||||||
raise errors.AnsibleFilterError('log() can only be used on numbers: %s' % str(e))
|
|
||||||
|
|
||||||
|
|
||||||
def power(x, y):
|
|
||||||
try:
|
|
||||||
return math.pow(x, y)
|
|
||||||
except TypeError, e:
|
|
||||||
raise errors.AnsibleFilterError('pow() can only be used on numbers: %s' % str(e))
|
|
||||||
|
|
||||||
|
|
||||||
def inversepower(x, base=2):
|
|
||||||
try:
|
|
||||||
if base == 2:
|
|
||||||
return math.sqrt(x)
|
|
||||||
else:
|
|
||||||
return math.pow(x, 1.0/float(base))
|
|
||||||
except TypeError, e:
|
|
||||||
raise errors.AnsibleFilterError('root() can only be used on numbers: %s' % str(e))
|
|
||||||
|
|
||||||
|
|
||||||
class FilterModule(object):
|
|
||||||
''' Ansible math jinja2 filters '''
|
|
||||||
|
|
||||||
def filters(self):
|
|
||||||
return {
|
|
||||||
# general math
|
|
||||||
'isnan': isnotanumber,
|
|
||||||
'min' : min,
|
|
||||||
'max' : max,
|
|
||||||
|
|
||||||
# exponents and logarithms
|
|
||||||
'log': logarithm,
|
|
||||||
'pow': power,
|
|
||||||
'root': inversepower,
|
|
||||||
|
|
||||||
# set theory
|
|
||||||
'unique' : unique,
|
|
||||||
'intersect': intersect,
|
|
||||||
'difference': difference,
|
|
||||||
'symmetric_difference': symmetric_difference,
|
|
||||||
'union': union,
|
|
||||||
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
# (c) 2013, Bradley Young <young.bradley@gmail.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/>.
|
|
||||||
|
|
||||||
import ansible.utils as utils
|
|
||||||
import ansible.errors as errors
|
|
||||||
from itertools import product
|
|
||||||
|
|
||||||
def flatten(terms):
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
if isinstance(term, list):
|
|
||||||
ret.extend(term)
|
|
||||||
elif isinstance(term, tuple):
|
|
||||||
ret.extend(term)
|
|
||||||
else:
|
|
||||||
ret.append(term)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
"""
|
|
||||||
Create the cartesian product of lists
|
|
||||||
[1, 2, 3], [a, b] -> [1, a], [1, b], [2, a], [2, b], [3, a], [3, b]
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def __lookup_injects(self, terms, inject):
|
|
||||||
results = []
|
|
||||||
for x in terms:
|
|
||||||
intermediate = utils.listify_lookup_plugin_terms(x, self.basedir, inject)
|
|
||||||
results.append(intermediate)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
terms = self.__lookup_injects(terms, inject)
|
|
||||||
|
|
||||||
my_list = terms[:]
|
|
||||||
if len(my_list) == 0:
|
|
||||||
raise errors.AnsibleError("with_cartesian requires at least one element in each list")
|
|
||||||
return [flatten(x) for x in product(*my_list)]
|
|
||||||
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
|||||||
# (c) 2015, Steve Gargan <steve.gargan@gmail.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/>.
|
|
||||||
|
|
||||||
'''
|
|
||||||
Lookup plugin to grab metadata from a consul key value store.
|
|
||||||
============================================================
|
|
||||||
|
|
||||||
Plugin will lookup metadata for a playbook from the key value store in a
|
|
||||||
consul cluster. Values can be easily set in the kv store with simple rest
|
|
||||||
commands e.g.
|
|
||||||
|
|
||||||
curl -X PUT -d 'some-value' http://localhost:8500/v1/kv/ansible/somedata
|
|
||||||
|
|
||||||
this can then be looked up in a playbook as follows
|
|
||||||
|
|
||||||
- debug: msg='key contains {{item}}'
|
|
||||||
with_consul_kv:
|
|
||||||
- 'key/to/retrieve'
|
|
||||||
|
|
||||||
|
|
||||||
Parameters can be provided after the key be more specific about what to retrieve e.g.
|
|
||||||
|
|
||||||
- debug: msg='key contains {{item}}'
|
|
||||||
with_consul_kv:
|
|
||||||
- 'key/to recurse=true token=E6C060A9-26FB-407A-B83E-12DDAFCB4D98')}}'
|
|
||||||
|
|
||||||
recurse: if true, will retrieve all the values that have the given key as prefix
|
|
||||||
index: if the key has a value with the specified index then this is returned
|
|
||||||
allowing access to historical values.
|
|
||||||
token: acl token to allow access to restricted values.
|
|
||||||
|
|
||||||
By default this will lookup keys via the consul agent running on http://localhost:8500
|
|
||||||
this can be changed by setting the env variable 'ANSIBLE_CONSUL_URL' to point to the url
|
|
||||||
of the kv store you'd like to use.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from urlparse import urlparse
|
|
||||||
from ansible import utils, errors
|
|
||||||
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
except ImportError:
|
|
||||||
import simplejson as json
|
|
||||||
|
|
||||||
try:
|
|
||||||
import consul
|
|
||||||
except ImportError, e:
|
|
||||||
print "failed=True msg='python-consul required for this module. "\
|
|
||||||
"see http://python-consul.readthedocs.org/en/latest/#installation'"
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
|
|
||||||
self.basedir = basedir
|
|
||||||
self.agent_url = 'http://localhost:8500'
|
|
||||||
if os.getenv('ANSIBLE_CONSUL_URL') is not None:
|
|
||||||
self.agent_url = os.environ['ANSIBLE_CONSUL_URL']
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
u = urlparse(self.agent_url)
|
|
||||||
consul_api = consul.Consul(host=u.hostname, port=u.port)
|
|
||||||
|
|
||||||
values = []
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
try:
|
|
||||||
for term in terms:
|
|
||||||
params = self.parse_params(term)
|
|
||||||
results = consul_api.kv.get(params['key'],
|
|
||||||
token=params['token'],
|
|
||||||
index=params['index'],
|
|
||||||
recurse=params['recurse'])
|
|
||||||
if results[1]:
|
|
||||||
# responds with a single or list of result maps
|
|
||||||
if isinstance(results[1], list):
|
|
||||||
for r in results[1]:
|
|
||||||
values.append(r['Value'])
|
|
||||||
else:
|
|
||||||
values.append(results[1]['Value'])
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError(
|
|
||||||
"Error locating '%s' in kv store. Error was %s" % (term, e))
|
|
||||||
|
|
||||||
return values
|
|
||||||
|
|
||||||
def parse_params(self, term):
|
|
||||||
params = term.split(' ')
|
|
||||||
|
|
||||||
paramvals = {
|
|
||||||
'key': params[0],
|
|
||||||
'token': None,
|
|
||||||
'recurse': False,
|
|
||||||
'index': None
|
|
||||||
}
|
|
||||||
|
|
||||||
# parameters specified?
|
|
||||||
try:
|
|
||||||
for param in params[1:]:
|
|
||||||
if param and len(param) > 0:
|
|
||||||
name, value = param.split('=')
|
|
||||||
assert name in paramvals, "% not a valid consul lookup parameter" % name
|
|
||||||
paramvals[name] = value
|
|
||||||
except (ValueError, AssertionError), e:
|
|
||||||
raise errors.AnsibleError(e)
|
|
||||||
|
|
||||||
return paramvals
|
|
@ -1,85 +0,0 @@
|
|||||||
# (c) 2013, Jan-Piet Mens <jpmens(at)gmail.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 import utils, errors
|
|
||||||
import os
|
|
||||||
import codecs
|
|
||||||
import csv
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def read_csv(self, filename, key, delimiter, dflt=None, col=1):
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = codecs.open(filename, 'r', encoding='utf-8')
|
|
||||||
creader = csv.reader(f, delimiter=delimiter)
|
|
||||||
|
|
||||||
for row in creader:
|
|
||||||
if row[0] == key:
|
|
||||||
return row[int(col)]
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError("csvfile: %s" % str(e))
|
|
||||||
|
|
||||||
return dflt
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
if isinstance(terms, basestring):
|
|
||||||
terms = [ terms ]
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
params = term.split()
|
|
||||||
key = params[0]
|
|
||||||
|
|
||||||
paramvals = {
|
|
||||||
'file' : 'ansible.csv',
|
|
||||||
'default' : None,
|
|
||||||
'delimiter' : "TAB",
|
|
||||||
'col' : "1", # column to return
|
|
||||||
}
|
|
||||||
|
|
||||||
# parameters specified?
|
|
||||||
try:
|
|
||||||
for param in params[1:]:
|
|
||||||
name, value = param.split('=')
|
|
||||||
assert(name in paramvals)
|
|
||||||
if name == 'delimiter':
|
|
||||||
paramvals[name] = str(value)
|
|
||||||
else:
|
|
||||||
paramvals[name] = value
|
|
||||||
except (ValueError, AssertionError), e:
|
|
||||||
raise errors.AnsibleError(e)
|
|
||||||
|
|
||||||
if paramvals['delimiter'] == 'TAB':
|
|
||||||
paramvals['delimiter'] = "\t"
|
|
||||||
|
|
||||||
path = utils.path_dwim(self.basedir, paramvals['file'])
|
|
||||||
|
|
||||||
var = self.read_csv(path, key, paramvals['delimiter'], paramvals['default'], paramvals['col'])
|
|
||||||
if var is not None:
|
|
||||||
if type(var) is list:
|
|
||||||
for v in var:
|
|
||||||
ret.append(v)
|
|
||||||
else:
|
|
||||||
ret.append(var)
|
|
||||||
return ret
|
|
@ -1,39 +0,0 @@
|
|||||||
# (c) 2014, Kent R. Spillner <kspillner@acm.org>
|
|
||||||
#
|
|
||||||
# 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.utils import safe_eval
|
|
||||||
import ansible.utils as utils
|
|
||||||
import ansible.errors as errors
|
|
||||||
|
|
||||||
def flatten_hash_to_list(terms):
|
|
||||||
ret = []
|
|
||||||
for key in terms:
|
|
||||||
ret.append({'key': key, 'value': terms[key]})
|
|
||||||
return ret
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
if not isinstance(terms, dict):
|
|
||||||
raise errors.AnsibleError("with_dict expects a dict")
|
|
||||||
|
|
||||||
return flatten_hash_to_list(terms)
|
|
@ -1,212 +0,0 @@
|
|||||||
# (c) 2015, Jan-Piet Mens <jpmens(at)gmail.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 import utils, errors
|
|
||||||
import socket
|
|
||||||
HAVE_DNS=False
|
|
||||||
try:
|
|
||||||
import dns.resolver
|
|
||||||
import dns.reversename
|
|
||||||
from dns.rdatatype import *
|
|
||||||
from dns.exception import DNSException
|
|
||||||
HAVE_DNS=True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def make_rdata_dict(rdata):
|
|
||||||
''' While the 'dig' lookup plugin supports anything which dnspython supports
|
|
||||||
out of the box, the following supported_types list describes which
|
|
||||||
DNS query types we can convert to a dict.
|
|
||||||
|
|
||||||
Note: adding support for RRSIG is hard work. :)
|
|
||||||
'''
|
|
||||||
supported_types = {
|
|
||||||
A : ['address'],
|
|
||||||
AAAA : ['address'],
|
|
||||||
CNAME : ['target'],
|
|
||||||
DNAME : ['target'],
|
|
||||||
DLV : ['algorithm', 'digest_type', 'key_tag', 'digest'],
|
|
||||||
DNSKEY : ['flags', 'algorithm', 'protocol', 'key'],
|
|
||||||
DS : ['algorithm', 'digest_type', 'key_tag', 'digest'],
|
|
||||||
HINFO : ['cpu', 'os'],
|
|
||||||
LOC : ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision'],
|
|
||||||
MX : ['preference', 'exchange'],
|
|
||||||
NAPTR : ['order', 'preference', 'flags', 'service', 'regexp', 'replacement'],
|
|
||||||
NS : ['target'],
|
|
||||||
NSEC3PARAM : ['algorithm', 'flags', 'iterations', 'salt'],
|
|
||||||
PTR : ['target'],
|
|
||||||
RP : ['mbox', 'txt'],
|
|
||||||
# RRSIG : ['algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'signature'],
|
|
||||||
SOA : ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'],
|
|
||||||
SPF : ['strings'],
|
|
||||||
SRV : ['priority', 'weight', 'port', 'target'],
|
|
||||||
SSHFP : ['algorithm', 'fp_type', 'fingerprint'],
|
|
||||||
TLSA : ['usage', 'selector', 'mtype', 'cert'],
|
|
||||||
TXT : ['strings'],
|
|
||||||
}
|
|
||||||
|
|
||||||
rd = {}
|
|
||||||
|
|
||||||
if rdata.rdtype in supported_types:
|
|
||||||
fields = supported_types[rdata.rdtype]
|
|
||||||
for f in fields:
|
|
||||||
val = rdata.__getattribute__(f)
|
|
||||||
|
|
||||||
if type(val) == dns.name.Name:
|
|
||||||
val = dns.name.Name.to_text(val)
|
|
||||||
|
|
||||||
if rdata.rdtype == DLV and f == 'digest':
|
|
||||||
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
|
||||||
if rdata.rdtype == DS and f == 'digest':
|
|
||||||
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
|
||||||
if rdata.rdtype == DNSKEY and f == 'key':
|
|
||||||
val = dns.rdata._base64ify(rdata.key).replace(' ', '')
|
|
||||||
if rdata.rdtype == NSEC3PARAM and f == 'salt':
|
|
||||||
val = dns.rdata._hexify(rdata.salt).replace(' ', '')
|
|
||||||
if rdata.rdtype == SSHFP and f == 'fingerprint':
|
|
||||||
val = dns.rdata._hexify(rdata.fingerprint).replace(' ', '')
|
|
||||||
if rdata.rdtype == TLSA and f == 'cert':
|
|
||||||
val = dns.rdata._hexify(rdata.cert).replace(' ', '')
|
|
||||||
|
|
||||||
|
|
||||||
rd[f] = val
|
|
||||||
|
|
||||||
return rd
|
|
||||||
|
|
||||||
# ==============================================================
|
|
||||||
# dig: Lookup DNS records
|
|
||||||
#
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
if HAVE_DNS == False:
|
|
||||||
raise errors.AnsibleError("Can't LOOKUP(dig): module dns.resolver is not installed")
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
'''
|
|
||||||
terms contains a string with things to `dig' for. We support the
|
|
||||||
following formats:
|
|
||||||
example.com # A record
|
|
||||||
example.com qtype=A # same
|
|
||||||
example.com/TXT # specific qtype
|
|
||||||
example.com qtype=txt # same
|
|
||||||
192.168.1.2/PTR # reverse PTR
|
|
||||||
^^ shortcut for 2.1.168.192.in-addr.arpa/PTR
|
|
||||||
example.net/AAAA @nameserver # query specified server
|
|
||||||
^^^ can be comma-sep list of names/addresses
|
|
||||||
|
|
||||||
... flat=0 # returns a dict; default is 1 == string
|
|
||||||
'''
|
|
||||||
terms = terms.split()
|
|
||||||
|
|
||||||
# Create Resolver object so that we can set NS if necessary
|
|
||||||
myres = dns.resolver.Resolver()
|
|
||||||
edns_size = 4096
|
|
||||||
myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
|
|
||||||
|
|
||||||
domain = None
|
|
||||||
qtype = 'A'
|
|
||||||
flat = True
|
|
||||||
|
|
||||||
for t in terms:
|
|
||||||
if t.startswith('@'): # e.g. "@10.0.1.2,192.168.1.1" is ok.
|
|
||||||
nsset = t[1:].split(',')
|
|
||||||
nameservers = []
|
|
||||||
for ns in nsset:
|
|
||||||
# Check if we have a valid IP address. If so, use that, otherwise
|
|
||||||
# try to resolve name to address using system's resolver. If that
|
|
||||||
# fails we bail out.
|
|
||||||
try:
|
|
||||||
socket.inet_aton(ns)
|
|
||||||
nameservers.append(ns)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
nsaddr = dns.resolver.query(ns)[0].address
|
|
||||||
nameservers.append(nsaddr)
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError("dns lookup NS: ", str(e))
|
|
||||||
myres.nameservers = nameservers
|
|
||||||
continue
|
|
||||||
if '=' in t:
|
|
||||||
try:
|
|
||||||
opt, arg = t.split('=')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if opt == 'qtype':
|
|
||||||
qtype = arg.upper()
|
|
||||||
elif opt == 'flat':
|
|
||||||
flat = int(arg)
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
if '/' in t:
|
|
||||||
try:
|
|
||||||
domain, qtype = t.split('/')
|
|
||||||
except:
|
|
||||||
domain = t
|
|
||||||
else:
|
|
||||||
domain = t
|
|
||||||
|
|
||||||
# print "--- domain = {0} qtype={1}".format(domain, qtype)
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
if qtype.upper() == 'PTR':
|
|
||||||
try:
|
|
||||||
n = dns.reversename.from_address(domain)
|
|
||||||
domain = n.to_text()
|
|
||||||
except dns.exception.SyntaxError:
|
|
||||||
pass
|
|
||||||
except Exception, e:
|
|
||||||
raise errors.AnsibleError("dns.reversename unhandled exception", str(e))
|
|
||||||
|
|
||||||
try:
|
|
||||||
answers = myres.query(domain, qtype)
|
|
||||||
for rdata in answers:
|
|
||||||
s = rdata.to_text()
|
|
||||||
if qtype.upper() == 'TXT':
|
|
||||||
s = s[1:-1] # Strip outside quotes on TXT rdata
|
|
||||||
|
|
||||||
if flat:
|
|
||||||
ret.append(s)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
rd = make_rdata_dict(rdata)
|
|
||||||
rd['owner'] = answers.canonical_name.to_text()
|
|
||||||
rd['type'] = dns.rdatatype.to_text(rdata.rdtype)
|
|
||||||
rd['ttl'] = answers.rrset.ttl
|
|
||||||
|
|
||||||
ret.append(rd)
|
|
||||||
except Exception, e:
|
|
||||||
ret.append(str(e))
|
|
||||||
|
|
||||||
except dns.resolver.NXDOMAIN:
|
|
||||||
ret.append('NXDOMAIN')
|
|
||||||
except dns.resolver.NoAnswer:
|
|
||||||
ret.append("")
|
|
||||||
except dns.resolver.Timeout:
|
|
||||||
ret.append('')
|
|
||||||
except dns.exception.DNSException, e:
|
|
||||||
raise errors.AnsibleError("dns.resolver unhandled exception", e)
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,68 +0,0 @@
|
|||||||
# (c) 2012, Jan-Piet Mens <jpmens(at)gmail.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 import utils, errors
|
|
||||||
import os
|
|
||||||
HAVE_DNS=False
|
|
||||||
try:
|
|
||||||
import dns.resolver
|
|
||||||
from dns.exception import DNSException
|
|
||||||
HAVE_DNS=True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ==============================================================
|
|
||||||
# DNSTXT: DNS TXT records
|
|
||||||
#
|
|
||||||
# key=domainname
|
|
||||||
# TODO: configurable resolver IPs
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
if HAVE_DNS == False:
|
|
||||||
raise errors.AnsibleError("Can't LOOKUP(dnstxt): module dns.resolver is not installed")
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
if isinstance(terms, basestring):
|
|
||||||
terms = [ terms ]
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
domain = term.split()[0]
|
|
||||||
string = []
|
|
||||||
try:
|
|
||||||
answers = dns.resolver.query(domain, 'TXT')
|
|
||||||
for rdata in answers:
|
|
||||||
s = rdata.to_text()
|
|
||||||
string.append(s[1:-1]) # Strip outside quotes on TXT rdata
|
|
||||||
|
|
||||||
except dns.resolver.NXDOMAIN:
|
|
||||||
string = 'NXDOMAIN'
|
|
||||||
except dns.resolver.Timeout:
|
|
||||||
string = ''
|
|
||||||
except dns.exception.DNSException, e:
|
|
||||||
raise errors.AnsibleError("dns.resolver unhandled exception", e)
|
|
||||||
|
|
||||||
ret.append(''.join(string))
|
|
||||||
return ret
|
|
@ -1,41 +0,0 @@
|
|||||||
# (c) 2012, Jan-Piet Mens <jpmens(at)gmail.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 import utils, errors
|
|
||||||
from ansible.utils import template
|
|
||||||
import os
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
try:
|
|
||||||
terms = template.template(self.basedir, terms, inject)
|
|
||||||
except Exception, e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if isinstance(terms, basestring):
|
|
||||||
terms = [ terms ]
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
var = term.split()[0]
|
|
||||||
ret.append(os.getenv(var, ''))
|
|
||||||
return ret
|
|
@ -1,78 +0,0 @@
|
|||||||
# (c) 2013, Jan-Piet Mens <jpmens(at)gmail.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 import utils
|
|
||||||
import os
|
|
||||||
import urllib2
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
except ImportError:
|
|
||||||
import simplejson as json
|
|
||||||
|
|
||||||
# this can be made configurable, not should not use ansible.cfg
|
|
||||||
ANSIBLE_ETCD_URL = 'http://127.0.0.1:4001'
|
|
||||||
if os.getenv('ANSIBLE_ETCD_URL') is not None:
|
|
||||||
ANSIBLE_ETCD_URL = os.environ['ANSIBLE_ETCD_URL']
|
|
||||||
|
|
||||||
class etcd():
|
|
||||||
def __init__(self, url=ANSIBLE_ETCD_URL):
|
|
||||||
self.url = url
|
|
||||||
self.baseurl = '%s/v1/keys' % (self.url)
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
url = "%s/%s" % (self.baseurl, key)
|
|
||||||
|
|
||||||
data = None
|
|
||||||
value = ""
|
|
||||||
try:
|
|
||||||
r = urllib2.urlopen(url)
|
|
||||||
data = r.read()
|
|
||||||
except:
|
|
||||||
return value
|
|
||||||
|
|
||||||
try:
|
|
||||||
# {"action":"get","key":"/name","value":"Jane Jolie","index":5}
|
|
||||||
item = json.loads(data)
|
|
||||||
if 'value' in item:
|
|
||||||
value = item['value']
|
|
||||||
if 'errorCode' in item:
|
|
||||||
value = "ENOENT"
|
|
||||||
except:
|
|
||||||
raise
|
|
||||||
pass
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
self.etcd = etcd()
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
if isinstance(terms, basestring):
|
|
||||||
terms = [ terms ]
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
key = term.split()[0]
|
|
||||||
value = self.etcd.get(key)
|
|
||||||
ret.append(value)
|
|
||||||
return ret
|
|
@ -1,59 +0,0 @@
|
|||||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.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 import utils, errors
|
|
||||||
import os
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
# this can happen if the variable contains a string, strictly not desired for lookup
|
|
||||||
# plugins, but users may try it, so make it work.
|
|
||||||
if not isinstance(terms, list):
|
|
||||||
terms = [ terms ]
|
|
||||||
|
|
||||||
for term in terms:
|
|
||||||
basedir_path = utils.path_dwim(self.basedir, term)
|
|
||||||
relative_path = None
|
|
||||||
playbook_path = None
|
|
||||||
|
|
||||||
# Special handling of the file lookup, used primarily when the
|
|
||||||
# lookup is done from a role. If the file isn't found in the
|
|
||||||
# basedir of the current file, use dwim_relative to look in the
|
|
||||||
# role/files/ directory, and finally the playbook directory
|
|
||||||
# itself (which will be relative to the current working dir)
|
|
||||||
if '_original_file' in inject:
|
|
||||||
relative_path = utils.path_dwim_relative(inject['_original_file'], 'files', term, self.basedir, check=False)
|
|
||||||
if 'playbook_dir' in inject:
|
|
||||||
playbook_path = os.path.join(inject['playbook_dir'], term)
|
|
||||||
|
|
||||||
for path in (basedir_path, relative_path, playbook_path):
|
|
||||||
if path and os.path.exists(path):
|
|
||||||
ret.append(codecs.open(path, encoding="utf8").read().rstrip())
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise errors.AnsibleError("could not locate file in lookup: %s" % term)
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,39 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
from ansible import utils
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
for term in terms:
|
|
||||||
|
|
||||||
dwimmed = utils.path_dwim(self.basedir, term)
|
|
||||||
globbed = glob.glob(dwimmed)
|
|
||||||
ret.extend(g for g in globbed if os.path.isfile(g))
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,194 +0,0 @@
|
|||||||
# (c) 2013, seth vidal <skvidal@fedoraproject.org> red hat, inc
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
|
|
||||||
# take a list of files and (optionally) a list of paths
|
|
||||||
# return the first existing file found in the paths
|
|
||||||
# [file1, file2, file3], [path1, path2, path3]
|
|
||||||
# search order is:
|
|
||||||
# path1/file1
|
|
||||||
# path1/file2
|
|
||||||
# path1/file3
|
|
||||||
# path2/file1
|
|
||||||
# path2/file2
|
|
||||||
# path2/file3
|
|
||||||
# path3/file1
|
|
||||||
# path3/file2
|
|
||||||
# path3/file3
|
|
||||||
|
|
||||||
# first file found with os.path.exists() is returned
|
|
||||||
# no file matches raises ansibleerror
|
|
||||||
# EXAMPLES
|
|
||||||
# - name: copy first existing file found to /some/file
|
|
||||||
# action: copy src=$item dest=/some/file
|
|
||||||
# with_first_found:
|
|
||||||
# - files: foo ${inventory_hostname} bar
|
|
||||||
# paths: /tmp/production /tmp/staging
|
|
||||||
|
|
||||||
# that will look for files in this order:
|
|
||||||
# /tmp/production/foo
|
|
||||||
# ${inventory_hostname}
|
|
||||||
# bar
|
|
||||||
# /tmp/staging/foo
|
|
||||||
# ${inventory_hostname}
|
|
||||||
# bar
|
|
||||||
|
|
||||||
# - name: copy first existing file found to /some/file
|
|
||||||
# action: copy src=$item dest=/some/file
|
|
||||||
# with_first_found:
|
|
||||||
# - files: /some/place/foo ${inventory_hostname} /some/place/else
|
|
||||||
|
|
||||||
# that will look for files in this order:
|
|
||||||
# /some/place/foo
|
|
||||||
# $relative_path/${inventory_hostname}
|
|
||||||
# /some/place/else
|
|
||||||
|
|
||||||
# example - including tasks:
|
|
||||||
# tasks:
|
|
||||||
# - include: $item
|
|
||||||
# with_first_found:
|
|
||||||
# - files: generic
|
|
||||||
# paths: tasks/staging tasks/production
|
|
||||||
# this will include the tasks in the file generic where it is found first (staging or production)
|
|
||||||
|
|
||||||
# example simple file lists
|
|
||||||
#tasks:
|
|
||||||
#- name: first found file
|
|
||||||
# action: copy src=$item dest=/etc/file.cfg
|
|
||||||
# with_first_found:
|
|
||||||
# - files: foo.${inventory_hostname} foo
|
|
||||||
|
|
||||||
|
|
||||||
# example skipping if no matched files
|
|
||||||
# First_found also offers the ability to control whether or not failing
|
|
||||||
# to find a file returns an error or not
|
|
||||||
#
|
|
||||||
#- name: first found file - or skip
|
|
||||||
# action: copy src=$item dest=/etc/file.cfg
|
|
||||||
# with_first_found:
|
|
||||||
# - files: foo.${inventory_hostname}
|
|
||||||
# skip: true
|
|
||||||
|
|
||||||
# example a role with default configuration and configuration per host
|
|
||||||
# you can set multiple terms with their own files and paths to look through.
|
|
||||||
# consider a role that sets some configuration per host falling back on a default config.
|
|
||||||
#
|
|
||||||
#- name: some configuration template
|
|
||||||
# template: src={{ item }} dest=/etc/file.cfg mode=0444 owner=root group=root
|
|
||||||
# with_first_found:
|
|
||||||
# - files:
|
|
||||||
# - ${inventory_hostname}/etc/file.cfg
|
|
||||||
# paths:
|
|
||||||
# - ../../../templates.overwrites
|
|
||||||
# - ../../../templates
|
|
||||||
# - files:
|
|
||||||
# - etc/file.cfg
|
|
||||||
# paths:
|
|
||||||
# - templates
|
|
||||||
|
|
||||||
# the above will return an empty list if the files cannot be found at all
|
|
||||||
# if skip is unspecificed or if it is set to false then it will return a list
|
|
||||||
# error which can be caught bye ignore_errors: true for that action.
|
|
||||||
|
|
||||||
# finally - if you want you can use it, in place to replace first_available_file:
|
|
||||||
# you simply cannot use the - files, path or skip options. simply replace
|
|
||||||
# first_available_file with with_first_found and leave the file listing in place
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# - name: with_first_found like first_available_file
|
|
||||||
# action: copy src=$item dest=/tmp/faftest
|
|
||||||
# with_first_found:
|
|
||||||
# - ../files/foo
|
|
||||||
# - ../files/bar
|
|
||||||
# - ../files/baz
|
|
||||||
# ignore_errors: true
|
|
||||||
|
|
||||||
|
|
||||||
from ansible import utils, errors
|
|
||||||
import os
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
result = None
|
|
||||||
anydict = False
|
|
||||||
skip = False
|
|
||||||
|
|
||||||
for term in terms:
|
|
||||||
if isinstance(term, dict):
|
|
||||||
anydict = True
|
|
||||||
|
|
||||||
total_search = []
|
|
||||||
if anydict:
|
|
||||||
for term in terms:
|
|
||||||
if isinstance(term, dict):
|
|
||||||
files = term.get('files', [])
|
|
||||||
paths = term.get('paths', [])
|
|
||||||
skip = utils.boolean(term.get('skip', False))
|
|
||||||
|
|
||||||
filelist = files
|
|
||||||
if isinstance(files, basestring):
|
|
||||||
files = files.replace(',', ' ')
|
|
||||||
files = files.replace(';', ' ')
|
|
||||||
filelist = files.split(' ')
|
|
||||||
|
|
||||||
pathlist = paths
|
|
||||||
if paths:
|
|
||||||
if isinstance(paths, basestring):
|
|
||||||
paths = paths.replace(',', ' ')
|
|
||||||
paths = paths.replace(':', ' ')
|
|
||||||
paths = paths.replace(';', ' ')
|
|
||||||
pathlist = paths.split(' ')
|
|
||||||
|
|
||||||
if not pathlist:
|
|
||||||
total_search = filelist
|
|
||||||
else:
|
|
||||||
for path in pathlist:
|
|
||||||
for fn in filelist:
|
|
||||||
f = os.path.join(path, fn)
|
|
||||||
total_search.append(f)
|
|
||||||
else:
|
|
||||||
total_search.append(term)
|
|
||||||
else:
|
|
||||||
total_search = terms
|
|
||||||
|
|
||||||
for fn in total_search:
|
|
||||||
if inject and '_original_file' in inject:
|
|
||||||
# check the templates and vars directories too,
|
|
||||||
# if they exist
|
|
||||||
for roledir in ('templates', 'vars'):
|
|
||||||
path = utils.path_dwim(os.path.join(self.basedir, '..', roledir), fn)
|
|
||||||
if os.path.exists(path):
|
|
||||||
return [path]
|
|
||||||
# if none of the above were found, just check the
|
|
||||||
# current filename against the basedir (this will already
|
|
||||||
# have ../files from runner, if it's a role task
|
|
||||||
path = utils.path_dwim(self.basedir, fn)
|
|
||||||
if os.path.exists(path):
|
|
||||||
return [path]
|
|
||||||
else:
|
|
||||||
if skip:
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
return [None]
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
|||||||
# (c) 2013, Serge van Ginderachter <serge@vanginderachter.be>
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
import ansible.utils as utils
|
|
||||||
import ansible.errors as errors
|
|
||||||
|
|
||||||
|
|
||||||
def check_list_of_one_list(term):
|
|
||||||
# make sure term is not a list of one (list of one..) item
|
|
||||||
# return the final non list item if so
|
|
||||||
|
|
||||||
if isinstance(term,list) and len(term) == 1:
|
|
||||||
term = term[0]
|
|
||||||
if isinstance(term,list):
|
|
||||||
term = check_list_of_one_list(term)
|
|
||||||
|
|
||||||
return term
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
|
|
||||||
def flatten(self, terms, inject):
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
term = check_list_of_one_list(term)
|
|
||||||
|
|
||||||
if term == 'None' or term == 'null':
|
|
||||||
# ignore undefined items
|
|
||||||
break
|
|
||||||
|
|
||||||
if isinstance(term, basestring):
|
|
||||||
# convert a variable to a list
|
|
||||||
term2 = utils.listify_lookup_plugin_terms(term, self.basedir, inject)
|
|
||||||
# but avoid converting a plain string to a list of one string
|
|
||||||
if term2 != [ term ]:
|
|
||||||
term = term2
|
|
||||||
|
|
||||||
if isinstance(term, list):
|
|
||||||
# if it's a list, check recursively for items that are a list
|
|
||||||
term = self.flatten(term, inject)
|
|
||||||
ret.extend(term)
|
|
||||||
else:
|
|
||||||
ret.append(term)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
|
|
||||||
# see if the string represents a list and convert to list if so
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
if not isinstance(terms, list):
|
|
||||||
raise errors.AnsibleError("with_flattened expects a list")
|
|
||||||
|
|
||||||
ret = self.flatten(terms, inject)
|
|
||||||
return ret
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
|||||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.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.utils import safe_eval
|
|
||||||
import ansible.utils as utils
|
|
||||||
import ansible.errors as errors
|
|
||||||
|
|
||||||
def flatten(terms):
|
|
||||||
ret = []
|
|
||||||
for term in terms:
|
|
||||||
if isinstance(term, list):
|
|
||||||
ret.extend(term)
|
|
||||||
else:
|
|
||||||
ret.append(term)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
class LookupModule(object):
|
|
||||||
|
|
||||||
def __init__(self, basedir=None, **kwargs):
|
|
||||||
self.basedir = basedir
|
|
||||||
|
|
||||||
def run(self, terms, inject=None, **kwargs):
|
|
||||||
terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject)
|
|
||||||
|
|
||||||
if not isinstance(terms, list):
|
|
||||||
raise errors.AnsibleError("with_indexed_items expects a list")
|
|
||||||
|
|
||||||
items = flatten(terms)
|
|
||||||
return zip(range(len(items)), items)
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue