File: //usr/lib/python2.7/site-packages/salt/returners/couchdb_return.py
# -*- coding: utf-8 -*-
'''
Simple returner for CouchDB. Optional configuration
settings are listed below, along with sane defaults:
.. code-block:: yaml
couchdb.db: 'salt'
couchdb.url: 'http://salt:5984/'
Alternative configuration values can be used by prefacing the configuration.
Any values not found in the alternative configuration will be pulled from
the default location:
.. code-block:: yaml
alternative.couchdb.db: 'salt'
alternative.couchdb.url: 'http://salt:5984/'
To use the couchdb returner, append ``--return couchdb`` to the salt command. Example:
.. code-block:: bash
salt '*' test.ping --return couchdb
To use the alternative configuration, append ``--return_config alternative`` to the salt command.
.. versionadded:: 2015.5.0
.. code-block:: bash
salt '*' test.ping --return couchdb --return_config alternative
To override individual configuration items, append --return_kwargs '{"key:": "value"}' to the salt command.
.. versionadded:: 2016.3.0
.. code-block:: bash
salt '*' test.ping --return couchdb --return_kwargs '{"db": "another-salt"}'
On concurrent database access
==============================
As this returner creates a couchdb document with the salt job id as document id
and as only one document with a given id can exist in a given couchdb database,
it is advised for most setups that every minion be configured to write to it own
database (the value of ``couchdb.db`` may be suffixed with the minion id),
otherwise multi-minion targeting can lead to losing output:
* the first returning minion is able to create a document in the database
* other minions fail with ``{'error': 'HTTP Error 409: Conflict'}``
'''
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import time
# Import 3rd-party libs
# pylint: disable=no-name-in-module,import-error
from salt.ext.six.moves.urllib.error import HTTPError
from salt.ext.six.moves.urllib.request import (
Request as _Request,
HTTPHandler as _HTTPHandler,
build_opener as _build_opener,
)
# pylint: enable=no-name-in-module,import-error
# Import Salt libs
import salt.utils.jid
import salt.utils.json
import salt.returners
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = 'couchdb'
def __virtual__():
return __virtualname__
def _get_options(ret=None):
'''
Get the couchdb options from salt.
'''
attrs = {'url': 'url',
'db': 'db'}
_options = salt.returners.get_returner_options(__virtualname__,
ret,
attrs,
__salt__=__salt__,
__opts__=__opts__)
if 'url' not in _options:
log.debug("Using default url.")
_options['url'] = "http://salt:5984/"
if 'db' not in _options:
log.debug("Using default database.")
_options['db'] = "salt"
return _options
def _generate_doc(ret):
'''
Create a object that will be saved into the database based on
options.
'''
# Create a copy of the object that we will return.
retc = ret.copy()
# Set the ID of the document to be the JID.
retc["_id"] = ret["jid"]
# Add a timestamp field to the document
retc["timestamp"] = time.time()
return retc
def _request(method, url, content_type=None, _data=None):
'''
Makes a HTTP request. Returns the JSON parse, or an obj with an error.
'''
opener = _build_opener(_HTTPHandler)
request = _Request(url, data=_data)
if content_type:
request.add_header('Content-Type', content_type)
request.get_method = lambda: method
try:
handler = opener.open(request)
except HTTPError as exc:
return {'error': '{0}'.format(exc)}
return salt.utils.json.loads(handler.read())
def returner(ret):
'''
Take in the return and shove it into the couchdb database.
'''
options = _get_options(ret)
# Check to see if the database exists.
_response = _request("GET", options['url'] + "_all_dbs")
if options['db'] not in _response:
# Make a PUT request to create the database.
_response = _request("PUT", options['url'] + options['db'])
# Confirm that the response back was simple 'ok': true.
if 'ok' not in _response or _response['ok'] is not True:
log.error('Unable to create database \'%s\'', options['db'])
log.error('Nothing logged! Lost data.')
return
log.info('Created database \'%s\'', options['db'])
# Call _generate_doc to get a dict object of the document we're going to
# shove into the database.
doc = _generate_doc(ret)
# Make the actual HTTP PUT request to create the doc.
_response = _request("PUT",
options['url'] + options['db'] + "/" + doc['_id'],
'application/json',
salt.utils.json.dumps(doc))
# Sanity check regarding the response..
if 'ok' not in _response or _response['ok'] is not True:
log.error('Unable to create document: \'%s\'', _response)
log.error('Nothing logged! Lost data.')
def get_jid(jid):
'''
Get the document with a given JID.
'''
options = _get_options(ret=None)
_response = _request("GET", options['url'] + options['db'] + '/' + jid)
if 'error' in _response:
log.error('Unable to get JID \'%s\' : \'%s\'', jid, _response)
return {}
return {_response['id']: _response}
def get_jids():
'''
List all the jobs that we have..
'''
options = _get_options(ret=None)
_response = _request("GET", options['url'] + options['db'] + "/_all_docs?include_docs=true")
# Make sure the 'total_rows' is returned.. if not error out.
if 'total_rows' not in _response:
log.error(
'Didn\'t get valid response from requesting all docs: %s',
_response
)
return {}
# Return the rows.
ret = {}
for row in _response['rows']:
# Because this shows all the documents in the database, including the
# design documents, verify the id is salt jid
jid = row['id']
if not salt.utils.jid.is_jid(jid):
continue
ret[jid] = salt.utils.jid.format_jid_instance(jid, row['doc'])
return ret
def get_fun(fun):
'''
Return a dict with key being minion and value
being the job details of the last run of function 'fun'.
'''
# Get the options..
options = _get_options(ret=None)
# Define a simple return object.
_ret = {}
# get_minions takes care of calling ensure_views for us.
# For each minion we know about
for minion in get_minions():
# Make a query of the by-minion-and-timestamp view and limit the count
# to 1.
_response = _request("GET",
options['url'] +
options['db'] +
('/_design/salt/_view/by-minion-fun-times'
'tamp?descending=true&endkey=["{0}","{1}'
'",0]&startkey=["{0}","{1}",9999999999]&'
'limit=1').format(minion, fun))
# Skip the minion if we got an error..
if 'error' in _response:
log.warning(
'Got an error when querying for last command by a minion: %s',
_response['error']
)
continue
# Skip the minion if we didn't get any rows back. ( IE function that
# they're looking for has a typo in it or some such ).
if len(_response['rows']) < 1:
continue
# Set the respnse ..
_ret[minion] = _response['rows'][0]['value']
return _ret
def get_minions():
'''
Return a list of minion identifiers from a request of the view.
'''
options = _get_options(ret=None)
# Make sure the views are valid, which includes the minions..
if not ensure_views():
return []
# Make the request for the view..
_response = _request("GET",
options['url'] +
options['db'] +
"/_design/salt/_view/minions?group=true")
# Verify that we got a response back.
if 'rows' not in _response:
log.error('Unable to get available minions: %s', _response)
return []
# Iterate over the rows to build up a list return it.
_ret = []
for row in _response['rows']:
_ret.append(row['key'])
return _ret
def ensure_views():
'''
This function makes sure that all the views that should
exist in the design document do exist.
'''
# Get the options so we have the URL and DB..
options = _get_options(ret=None)
# Make a request to check if the design document exists.
_response = _request("GET",
options['url'] + options['db'] + "/_design/salt")
# If the document doesn't exist, or for some reason there are not views.
if 'error' in _response:
return set_salt_view()
# Determine if any views are missing from the design doc stored on the
# server.. If we come across one, simply set the salt view and return out.
# set_salt_view will set all the views, so we don't need to continue t
# check.
for view in get_valid_salt_views():
if view not in _response['views']:
return set_salt_view()
# Valid views, return true.
return True
def get_valid_salt_views():
'''
Returns a dict object of views that should be
part of the salt design document.
'''
ret = {}
ret['minions'] = {}
ret['minions']['map'] = "function( doc ){ emit( doc.id, null ); }"
ret['minions']['reduce'] = \
"function( keys,values,rereduce ){ return key[0]; }"
ret['by-minion-fun-timestamp'] = {}
ret['by-minion-fun-timestamp']['map'] = \
"function( doc ){ emit( [doc.id,doc.fun,doc.timestamp], doc ); }"
return ret
def set_salt_view():
'''
Helper function that sets the salt design
document. Uses get_valid_salt_views and some hardcoded values.
'''
options = _get_options(ret=None)
# Create the new object that we will shove in as the design doc.
new_doc = {}
new_doc['views'] = get_valid_salt_views()
new_doc['language'] = "javascript"
# Make the request to update the design doc.
_response = _request("PUT",
options['url'] + options['db'] + "/_design/salt",
"application/json", salt.utils.json.dumps(new_doc))
if 'error' in _response:
log.warning(
'Unable to set the salt design document: %s',
_response['error']
)
return False
return True
def prep_jid(nocache=False, passed_jid=None): # pylint: disable=unused-argument
'''
Do any work necessary to prepare a JID, including sending a custom id
'''
return passed_jid if passed_jid is not None else salt.utils.jid.gen_jid(__opts__)
def save_minions(jid, minions, syndic_id=None): # pylint: disable=unused-argument
'''
Included for API consistency
'''