Proxy minion support.

This commit is contained in:
C. R. Oldham 2014-01-15 13:00:59 -07:00 committed by C. R. Oldham
parent 552ff8308b
commit 637901e23e
6 changed files with 143 additions and 103 deletions

View file

@ -2,10 +2,11 @@
Salt Proxy Minion Documentation
===============================
Proxy minions are a Salt feature that enables controlling devices that, for
whatever reason, cannot run a standard salt-minion. Examples include network
gear that has an API but runs a proprietary OS, devices with limited CPU or
memory, or devices that could run a minion, but for security reasons, will not.
Proxy minions are a developing Salt feature that enables controlling devices
that, for whatever reason, cannot run a standard salt-minion. Examples include
network gear that has an API but runs a proprietary OS, devices with limited
CPU or memory, or devices that could run a minion, but for security reasons,
will not.
*Proxy minions are not an "out of the box" feature*. Because
there are an infinite number of controllable devices,
@ -41,21 +42,15 @@ any way with the minion that started it.
Configuration parameters on the master
######################################
In ``/etc/salt/master``, add the following key:
Proxy minions require no configuration parameters in /etc/salt/master.
.. code-block:: yaml
enumerate_proxy_minions: True
This will invoke the salt-master routines to look for other configuration parameters
to drive the proxies.
Salt's Pillar system is ideally suited for configuring proxy-minions. Proxies can
either be designated via a pillar file in pillar_roots, or through an external pillar.
External pillars afford the opportunity for interfacing with a configuration management
system, database, or other knowledgeable system that that may already contain all the details
of proxy targets. To use static files in pillar_roots, pattern your files after the following
examples, which are based on the diagram above:
Salt's Pillar system is ideally suited for configuring proxy-minions. Proxies
can either be designated via a pillar file in pillar_roots, or through an
external pillar. External pillars afford the opportunity for interfacing with
a configuration management system, database, or other knowledgeable system that
that may already contain all the details of proxy targets. To use static files
in pillar_roots, pattern your files after the following examples, which are
based on the diagram above:
``/srv/salt/pillar/top.sls``
@ -145,22 +140,26 @@ to enable control.
Proxytypes
##########
A proxytype is a Python file that encapsulates all the code necessary to interface with
a device. Proxytypes are located inside the salt.proxy module.
At a minimum a proxytype must implement the following functions:
A proxytype is a Python class called 'Proxyconn' that encapsulates all the code
necessary to interface with a device. Proxytypes are located inside the
salt.proxy module. At a minimum a proxytype object must implement the
following methods:
``proxytype()``: Returns a string with the name of the proxy type.
``proxytype(self)``: Returns a string with the name of the proxy type.
``proxyconn(*args, **kwargs)``: Provides the primary way to connect and communicate
``proxyconn(self, **kwargs)``: Provides the primary way to connect and communicate
with the device. Some proxyconns instantiate a particular object that opens a
network connection to a device and leaves the connection open for communication.
Others simply abstract a serial connection or even implement endpoints to communicate
via REST over HTTP.
``id(opts)``: Returns a unique, unchanging id for the controlled device. This is
``id(self, opts)``: Returns a unique, unchanging id for the controlled device. This is
the "name" of the device, and is used by the salt-master for targeting and key
authentication.
Optionally, the class may define a ``shutdown(self, opts)`` method if the
controlled device should be informed when the minion goes away cleanly.
It is highly recommended that the ``test.ping`` execution module also be defined
for a proxytype. The code for ``ping`` should contact the controlled device and make
sure it is really available.
@ -170,6 +169,7 @@ the Junos operating system. Note the additional library requirements--most of t
"hard part" of talking to these devices is handled by the jnpr.junos, jnpr.junos.utils
and jnpr.junos.cfg modules.
.. code-block:: python
# Import python libs
@ -181,32 +181,49 @@ and jnpr.junos.cfg modules.
import jnpr.junos.cfg
HAS_JUNOS = True
def proxyconn(user=None, host=None, passwd=None):
jdev = jnpr.junos.Device(user=user, host=host, password=passwd)
jdev.open()
jdev.bind(cu=jnpr.junos.utils.Config)
return jdev
class Proxyconn(object):
def proxytype():
return 'junos'
def id(opts):
return opts['proxyconn'].facts['hostname']
def __init__(self, details):
self.conn = jnpr.junos.Device(user=details['username'], host=details['host'], password=details['passwd'])
self.conn.open()
self.conn.bind(cu=jnpr.junos.cfg.Resource)
def proxytype(self):
return 'junos'
def id(self, opts):
return self.conn.facts['hostname']
def ping(self):
return self.conn.connected
def shutdown(self, opts):
print('Proxy module {} shutting down!!'.format(opts['id']))
try:
self.conn.close()
except Exception:
pass
The __proxyenabled__ directive
##############################
Salt states and execution modules, by and large, cannot "automatically" work with
proxied devices. Execution modules like ``pkg`` or ``sqlite3`` have no meaning on
a network switch or a housecat. For a state/execution module to be available to
a proxy-minion, the ``__proxyenabled__`` variable must be defined in the module as an
array containing the names of all the proxytypes that this module can support. The
array can contain the special value ``*`` to indicate that the module supports all
proxies.
Salt states and execution modules, by and large, cannot "automatically" work
with proxied devices. Execution modules like ``pkg`` or ``sqlite3`` have no
meaning on a network switch or a housecat. For a state/execution module to be
available to a proxy-minion, the ``__proxyenabled__`` variable must be defined
in the module as an array containing the names of all the proxytypes that this
module can support. The array can contain the special value ``*`` to indicate
that the module supports all proxies.
If no ``__proxyenabled__`` variable is defined, then by default, the state/execution
module is unavailable to any proxy.
If no ``__proxyenabled__`` variable is defined, then by default, the
state/execution module is unavailable to any proxy.
Here is an excerpt from a module that was modified to support proxy-minions:
@ -214,10 +231,9 @@ Here is an excerpt from a module that was modified to support proxy-minions:
def ping():
if 'proxytype' in __opts__:
fun = 'salt.proxy.{0}.ping'.format(__opts__['proxytype'])
if fun in __salt__:
return __salt__[fun]()
if 'proxyobject' in __opts__:
if 'ping' in __opts__['proxyobject'].__attr__():
return __opts['proxyobject'].ping()
else:
return False
else:
@ -227,21 +243,11 @@ And then in salt.proxy.junos we find
.. code-block:: python
def ping():
def ping(self):
return self.connected
try:
got = junos.rpc.get_config(
E.system(
E('host-name'),
E('domain-name')
),
JXML.INHERIT
)
return True
except Exception:
return False
The Junos API layer lacks the ability to do a traditional 'ping', so the
example simply requests a very small bit of data via RPC. If the RPC call
throws an exception or times out, the device is unavailable and ping returns
False. Otherwise, we can return True.
example simply checks the connection object field that indicates
if the ssh connection was successfully made to the device.

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8 -*-
'''
Make me some salt!
'''
@ -239,7 +239,7 @@ class ProxyMinion(parsers.MinionOptionParser):
'''
Create a proxy minion server
'''
def prepare(self):
def prepare(self, proxydetails):
'''
Run the preparation sequence required to start a salt minion.
@ -280,7 +280,10 @@ class ProxyMinion(parsers.MinionOptionParser):
permissive=self.config['permissive_pki_access'],
pki_dir=self.config['pki_dir'],
)
logfile = self.config['log_file']
if 'proxy_log' in proxydetails:
logfile = proxydetails['proxy_log']
else:
logfile = None
if logfile is not None and not logfile.startswith(('tcp://',
'udp://',
'file://')):
@ -289,6 +292,7 @@ class ProxyMinion(parsers.MinionOptionParser):
except OSError as err:
sys.exit(err.errno)
self.config['proxy'] = proxydetails
self.setup_logfile_logger()
logger.info(
'Setting up a Salt Proxy Minion "{0}"'.format(
@ -309,7 +313,7 @@ class ProxyMinion(parsers.MinionOptionParser):
else:
self.minion = salt.minion.ProxyMinion(self.config)
def start(self):
def start(self, proxydetails):
'''
Start the actual minion.
@ -319,23 +323,26 @@ class ProxyMinion(parsers.MinionOptionParser):
NOTE: Run any required code before calling `super()`.
'''
self.prepare()
try:
if check_user(self.config['user']):
self.minion.tune_in()
except (KeyboardInterrupt, SaltSystemExit) as exc:
logger.warn('Stopping the Salt Minion')
if isinstance(exc, KeyboardInterrupt):
logger.warn('Exiting on Ctrl-c')
else:
logger.error(str(exc))
finally:
self.shutdown()
self.prepare(proxydetails)
self.minion.tune_in()
# try:
# if check_user(self.config['user']):
# except (KeyboardInterrupt, SaltSystemExit) as exc:
# logger.warn('Stopping the Salt Minion')
# if isinstance(exc, KeyboardInterrupt):
# logger.warn('Exiting on Ctrl-c')
# else:
# logger.error(str(exc))
# finally:
# self.shutdown()
def shutdown(self):
'''
If sub-classed, run any shutdown operations on this method.
'''
if 'proxy' in self.minion.opts:
self.minion.opts['proxyobject'].shutdown(self.minion.opts)
class Syndic(parsers.SyndicOptionParser):

View file

@ -181,6 +181,7 @@ VALID_OPTS = {
'sign_pub_messages': bool,
'keysize': int,
'salt_transport': str,
'enumerate_proxy_minions': bool,
}
# default configurations
@ -383,6 +384,7 @@ DEFAULT_MASTER_OPTS = {
'sign_pub_messages': False,
'keysize': 4096,
'salt_transport': 'zeromq',
'enumerate_proxy_minions': False
}
# ----- Salt Cloud Configuration Defaults ----------------------------------->

View file

@ -19,6 +19,7 @@ import traceback
import sys
import signal
from random import randint
import salt
import importlib
# Import third party libs
@ -537,8 +538,22 @@ class Minion(object):
self.opts,
self.functions,
self.returners)
self.grains_cache = self.opts['grains']
if 'proxy' in self.opts['pillar']:
log.debug('I am {} and I need to start some proxies for {}'.format(self.opts['id'], self.opts['pillar']['proxy']))
for p in self.opts['pillar']['proxy']:
log.debug('Starting {} proxy.'.format(p))
pid = os.fork()
if pid > 0:
continue
else:
proxyminion = salt.ProxyMinion()
proxyminion.start(self.opts['pillar']['proxy'][p])
else:
log.debug("I am {} and I am not supposed to start any proxies.".format(self.opts['id']))
def __prep_mod_opts(self):
'''
Returns a copy of the opts with key bits stripped out
@ -1852,11 +1867,10 @@ class ProxyMinion(object):
# Late setup the of the opts grains, so we can log from the grains
# module
# print opts['proxymodule']
fq_proxyname = 'proxy.'+opts['proxymodule']
proxymodule = salt.loader.proxy(opts, fq_proxyname)
opts['proxytype'] = proxymodule[opts['proxymodule']+'.proxytype']()
opts['proxyconn'] = proxymodule[opts['proxymodule']+'.proxyconn'](user='cro', host='junos', passwd='croldham123')
opts['id'] = proxymodule[opts['proxymodule']+'.id'](opts)
fq_proxyname = 'proxy.'+opts['proxy']['proxytype']
self.proxymodule = salt.loader.proxy(opts, fq_proxyname)
opts['proxyobject'] = self.proxymodule[opts['proxy']['proxytype']+'.Proxyconn'](opts['proxy'])
opts['id'] = opts['proxyobject'].id(opts)
opts.update(resolve_dns(opts))
self.opts = opts
self.authenticate(timeout, safe)
@ -1878,7 +1892,6 @@ class ProxyMinion(object):
self.grains_cache = self.opts['grains']
def __prep_mod_opts(self):
'''
Returns a copy of the opts with key bits stripped out
@ -2509,14 +2522,14 @@ class ProxyMinion(object):
self.opts['id'],
time.asctime()
),
'minion_start'
'proxy_minion_start'
)
self._fire_master(
'Minion {0} started at {1}'.format(
self.opts['id'],
time.asctime()
),
tagify([self.opts['id'], 'start'], 'minion'),
tagify([self.opts['id'], 'start'], 'proxy_minion'),
)
# Make sure to gracefully handle SIGUSR1
@ -2640,11 +2653,11 @@ class ProxyMinion(object):
self.opts['id'],
time.asctime()
),
'minion_start'
'proxy_minion_start'
)
# dup name spaced event
self._fire_master(
'Minion {0} started at {1}'.format(
'Proxy Minion {0} started at {1}'.format(
self.opts['id'],
time.asctime()
),
@ -2665,7 +2678,7 @@ class ProxyMinion(object):
yield True
except Exception:
log.critical(
'An exception occurred while polling the minion',
'An exception occurred while polling the proxy minion',
exc_info=True
)
yield True

View file

@ -41,12 +41,8 @@ def ping():
salt '*' test.ping
'''
if 'proxytype' in __opts__:
fun = 'salt.proxy.{0}.ping'.format(__opts__['proxytype'])
if fun in __salt__:
return __salt__[fun]()
else:
return False
if 'proxyobject' in __opts__:
return __opts__['proxyobject'].ping()
else:
return True

View file

@ -12,15 +12,31 @@ import jnpr.junos.utils
import jnpr.junos.cfg
HAS_JUNOS = True
class Proxyconn(object):
def proxyconn(user=None, host=None, passwd=None):
jdev = jnpr.junos.Device(user=user, host=host, password=passwd)
jdev.open()
jdev.bind(cu=jnpr.junos.utils.Config)
return jdev
def proxytype():
return 'junos'
def __init__(self, details):
self.conn = jnpr.junos.Device(user=details['username'], host=details['host'], password=details['passwd'])
self.conn.open()
self.conn.bind(cu=jnpr.junos.cfg.Resource)
def id(opts):
return opts['proxyconn'].facts['hostname']
def proxytype(self):
return 'junos'
def id(self, opts):
return self.conn.facts['hostname']
def ping(self):
return self.conn.connected
def shutdown(self, opts):
print('Proxy module {} shutting down!!'.format(opts['id']))
try:
self.conn.close()
except Exception:
pass