Merge pull request #27922 from cro/fx2

WIP States/Modules for managing Dell FX2 chassis via salt-proxy
This commit is contained in:
C. R. Oldham 2015-10-19 17:29:21 -06:00
commit 1085eeab2b
11 changed files with 1822 additions and 0 deletions

View file

@ -53,6 +53,7 @@ Full list of builtin execution modules
cabal
cassandra
cassandra_cql
chassis
chef
chocolatey
cloud
@ -87,6 +88,7 @@ Full list of builtin execution modules
dockerng
dpkg
drac
dracr
drbd
ebuild
eix

View file

@ -0,0 +1,6 @@
====================
salt.modules.chassis
====================
.. automodule:: salt.modules.chassis
:members:

View file

@ -0,0 +1,6 @@
==================
salt.modules.dracr
==================
.. automodule:: salt.modules.dracr
:members:

View file

@ -10,5 +10,6 @@ Full list of builtin proxy modules
:toctree:
:template: autosummary.rst.tmpl
fx2
junos
rest_sample

View file

@ -0,0 +1,6 @@
==============
salt.proxy.fx2
==============
.. automodule:: salt.proxy.fx2
:members:

View file

@ -50,6 +50,7 @@ Full list of builtin state modules
cyg
ddns
debconfmod
dellchassis
disk
dockerio
dockerng

View file

@ -0,0 +1,6 @@
=======================
salt.states.dellchassis
=======================
.. automodule:: salt.states.dellchassis
:members:

44
salt/modules/chassis.py Normal file
View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
'''
Glue execution module to link to the :doc:`fx2 proxymodule </ref/proxy/all/salt.proxy.fx2>`.
Depends: :doc:`iDRAC Remote execution module (salt.modules.dracr) </ref/modules/all/salt.modules.dracr>`
For documentation on commands that you can direct to a Dell chassis via proxy,
look in the documentation for :doc:`salt.modules.dracr </ref/modules/all/salt.modules.dracr>`.
This execution module calls through to a function in the fx2 proxy module
called ``chconfig``. That function looks up the function passed in the ``cmd``
parameter in :doc:`salt.modules.dracr </ref/modules/all/salt.modules.dracr>` and calls it.
.. versionadded:: 2015.8.2
'''
from __future__ import absolute_import
# Import python libs
import logging
import salt.utils
log = logging.getLogger(__name__)
__proxyenabled__ = ['fx2']
__virtualname__ = 'chassis'
def __virtual__():
'''
Only work on proxy
'''
if salt.utils.is_proxy():
return __virtualname__
return False
def cmd(cmd, *args, **kwargs):
proxycmd = __opts__['proxy']['proxytype'] + '.chconfig'
kwargs['admin_username'] = __pillar__['proxy']['admin_username']
kwargs['admin_password'] = __pillar__['proxy']['admin_password']
kwargs['host'] = __pillar__['proxy']['host']
return __proxy__[proxycmd](cmd, *args, **kwargs)

1173
salt/modules/dracr.py Normal file

File diff suppressed because it is too large Load diff

252
salt/proxy/fx2.py Normal file
View file

@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
'''
======
fx2.py
======
.. versionadded:: 2015.8.2
Proxy minion interface module for managing Dell FX2 chassis (Dell
Chassis Management Controller version 1.2 and above, iDRAC8 version 2.00
and above)
Dependencies
------------
- :doc:`iDRAC Remote execution module (salt.modules.dracr) </ref/modules/all/salt.modules.dracr>`
- :doc:`Chassis command shim (salt.modules.chassis) </ref/modules/all/salt.modules.chassis>`
- :doc:`Dell Chassis States (salt.states.dellchassis) </ref/states/all/salt.states.dellchassis>`
- Dell's ``racadm`` command line interface to CMC and iDRAC devices.
**Special Note: SaltStack thanks** `Adobe Corporation <http://adobe.com/>`_
**for their support in creating this proxy minion integration.**
This proxy minion enables Dell FX2 and FX2s (hereafter referred to as
simply "chassis", "CMC", or "FX2") chassis to be treated individually
like a salt-minion.
Since the CMC embedded in the chassis does not run an OS capable of hosting a
Python stack, the chassis can't run a minion directly. Salt's "Proxy Minion"
functionality enables you to designate another machine to host a minion
process that "proxies" communication from the salt-master. The master does not
know nor care that the target is not a real minion.
More in-depth conceptual reading on Proxy Minions can be found
:doc:`in the Proxy Minion section </topics/proxyminion/index>` of
Salt's documentation.
To configure this integration, follow these steps:
Pillar
------
Proxy minions get their configuration from Salt's Pillar. Every proxy must
have a stanza in Pillar, and a reference in the Pillar topfile that matches
the ID. At a minimum for communication with the chassis the pillar should
look like this:
.. code-block:: yaml
proxy:
host: <ip or dns name of chassis controller>
admin_username: <iDRAC username for the CMC, usually 'root'>
admin_password: <iDRAC password. Dell default is 'calvin'>
proxytype: fx2
The ``proxytype`` line above is critical, it tells Salt which interface to load
from the ``proxy`` directory in Salt's install hierarchy, or from ``/srv/salt/_proxy``
on the salt-master (if you have created your own proxy module, for example).
salt-proxy
----------
After your pillar is in place, you can test the proxy. The proxy can run on
any machine that has network connectivity to your salt-master and to the chassis in question.
SaltStack recommends that this machine also run a regular minion, though
it is not strictly necessary.
On the machine that will run the proxy, make sure there is an ``/etc/salt/proxy``
file with at least the following in it:
.. code-block:: yaml
master: <ip or hostname of salt-master>
You can start the proxy with
.. code-block:: bash
salt-proxy --proxyid <id you want to give the chassis>
You may want to add ``-l debug`` to run the above in the foreground in debug
mode just to make sure everything is OK.
Next, accept the key for the proxy on your salt-master, just like you would
for a regular minion:
.. code-block:: bash
salt-key -a <id you want to give the chassis>
You can confirm that the pillar data is in place for the proxy:
.. code-block:: bash
salt <id> pillar.items
And now you should be able to ping the chassis to make sure it is responding:
.. code-block:: bash
salt <id> test.ping
At this point you can execute one-off commands against the chassis. For
example, you can get the chassis inventory:
.. code-block:: bash
salt <id> chassis.cmd inventory
Note that you don't need to provide credentials or an ip/hostname. Salt knows
to use the credentials you stored in Pillar.
It's important to understand how this particular proxy works.
:doc:`Salt.modules.dracr </ref/modules/all/salt.modules.dracr>` is a standard Salt execution
module. If you pull up the docs for it you'll see that almost every function
in the module takes credentials and a target host. When credentials and a host
aren't passed, Salt runs ``racadm`` against the local machine. If you wanted
you could run functions from this module on any host where an appropriate
version of ``racadm`` is installed, and that host would reach out over the network
and communicate with the chassis.
``Chassis.cmd`` acts as a "shim" between the execution module and the proxy. It's
first parameter is always the function from salt.modules.dracr to execute. If the
function takes more positional or keyword arguments you can append them to the call.
It's this shim that speaks to the chassis through the proxy, arranging for the
credentials and hostname to be pulled from the pillar section for this proxy minion.
Because of the presence of the shim, to lookup documentation for what
functions you can use to interface with the chassis, you'll want to
look in :doc:`salt.modules.dracr </ref/modules/all/salt.modules.dracr>` instead
of :doc:`salt.modules.chassis </ref/modules/all/salt.modules.chassis>`.
States
------
Associated states are thoroughly documented in :doc:`salt.states.dellchassis </ref/states/all/salt.states.dellchassis>`.
Look there to find an example structure for pillar as well as an example
``.sls`` file for standing up a Dell Chassis from scratch.
'''
from __future__ import absolute_import
# Import python libs
import logging
import salt.utils
import salt.utils.http
# This must be present or the Salt loader won't load this module
__proxyenabled__ = ['fx2']
# Variables are scoped to this module so we can have persistent data
# across calls to fns in here.
GRAINS_CACHE = {}
DETAILS = {}
# Want logging!
log = logging.getLogger(__file__)
def __virtual__():
'''
Only return if all the modules are available
'''
if not salt.utils.which('racadm'):
log.critical('fx2 proxy minion needs "racadm" to be installed.')
return False
return True
def init(opts):
'''
This function gets called when the proxy starts up. For
FX2 devices we just cache the credentials and hostname.
'''
# Save the login details
DETAILS['admin_username'] = opts['proxy']['admin_username']
DETAILS['admin_password'] = opts['proxy']['admin_password']
DETAILS['host'] = opts['proxy']['host']
def grains():
'''
Get the grains from the proxied device
'''
if not GRAINS_CACHE:
r = __salt__['dracr.system_info'](host=DETAILS['host'],
admin_username=DETAILS['admin_username'],
admin_password=DETAILS['admin_password'])
GRAINS_CACHE = r
return GRAINS_CACHE
def grains_refresh():
'''
Refresh the grains from the proxied device
'''
GRAINS_CACHE = {}
return grains()
def chconfig(cmd, *args, **kwargs):
'''
This function is called by the :doc:`salt.modules.chassis.cmd </ref/modules/all/salt.modules.chassis>`
shim. It then calls whatever is passed in ``cmd``
inside the :doc:`salt.modules.dracr </ref/modules/all/salt.modules.dracr>`
module.
:param cmd: The command to call inside salt.modules.dracr
:param args: Arguments that need to be passed to that command
:param kwargs: Keyword arguments that need to be passed to that command
:return: Passthrough the return from the dracr module.
'''
# Strip the __pub_ keys...is there a better way to do this?
for k in kwargs.keys():
if k.startswith('__pub_'):
kwargs.pop(k)
if 'dracr.'+cmd not in __salt__:
return {'retcode': -1, 'message': 'dracr.' + cmd + ' is not available'}
else:
return __salt__['dracr.'+cmd](*args, **kwargs)
def ping():
'''
Is the chassis responding?
:return: Returns False if the chassis didn't respond, True otherwise.
'''
r = __salt__['dracr.system_info'](host=DETAILS['host'],
admin_username=DETAILS['admin_username'],
admin_password=DETAILS['admin_password'])
if r.get('retcode', 0) == 1:
return False
else:
return True
try:
return r['dict'].get('ret', False)
except Exception:
return False
def shutdown(opts):
'''
Shutdown the connection to the proxied device.
For this proxy shutdown is a no-op.
'''
log.debug('fx2 proxy shutdown() called...')

325
salt/states/dellchassis.py Normal file
View file

@ -0,0 +1,325 @@
# -*- coding: utf-8 -*-
'''
Manage chassis via Salt Proxies.
.. versionadded:: 2015.8.2
Example managing a Dell chassis:
.. code-block:: yaml
my-dell-chassis:
chassis.dell:
- name: my-dell-chassis
- location: my-location
- mode: 2
- idrac_launch: 1
- slot_names:
- 1: my-slot-name
- 2: my-other-slot-name
- blade_power_states:
- server-1: on
- server-2: off
- server-3: powercycle
'''
# Import python libs
from __future__ import absolute_import
import logging
log = logging.getLogger(__name__)
def __virtual__():
return 'chassis.cmd' in __salt__
def blade_idrac(name, idrac_password=None, idrac_ipmi=None,
idrac_ip=None, idrac_netmask=None, idrac_gateway=None,
drac_dhcp=None):
'''
Set parameters for iDRAC in a blade.
:param name: The name of the blade to address
:param idrac_password: Password to establish for the iDRAC interface
:param idrac_ipmi: Enable/Disable IPMI over LAN
:param idrac_ip: Set IP address for iDRAC
:param idrac_netmask: Set netmask for iDRAC
:param idrac_gateway: Set gateway for iDRAC
:param drac_dhcp: Turn on DHCP for iDRAC (True turns on, False does nothing
becaause setting a static IP will disable DHCP).
:return: A standard Salt changes dictionary
'''
pass
def chassis(name, location=None, mode=None, idrac_launch=None, slot_names=None,
blade_power_states=None):
'''
Manage a Dell Chassis.
name
The name of the chassis.
location
The location of the chassis.
mode
The management mode of the chassis. Viable options are:
- 0: None
- 1: Monitor
- 2: Manage and Monitor
idrac_launch
The iDRAC launch method of the chassis. Viable options are:
- 0: Disabled (launch iDRAC using IP address)
- 1: Enabled (launch iDRAC using DNS name)
slot_names
The names of the slots, provided as a list identified by
their slot numbers.
blade_power_states
The power states of a blade server, provided as a list and
identified by their server numbers. Viable options are:
- on: Ensure the blade server is powered on.
- off: Ensure the blade server is powered off.
- powercycle: Power cycle the blade server.
Example:
.. code-block:: yaml
my-dell-chassis:
chassis.dell:
- name: my-dell-chassis
- location: my-location
- mode: 2
- idrac_launch: 1
- slot_names:
- 1: my-slot-name
- 2: my-other-slot-name
- blade_power_states:
- server-1: on
- server-2: off
- server-3: powercycle
'''
ret = {'name': name,
'result': True,
'changes': {},
'comment': ''}
chassis_cmd = 'chassis.cmd'
cfg_tuning = 'cfgRacTuning'
mode_cmd = 'cfgRacTuneChassisMgmtAtServer'
launch_cmd = 'cfgRacTuneIdracDNSLaunchEnable'
current_name = __salt__[chassis_cmd]('get_chassis_name')
if name != current_name:
ret['changes'].update({'Name':
{'Old': current_name,
'New': name}})
if location:
current_location = __salt__[chassis_cmd]('get_chassis_location')
if location != current_location:
ret['changes'].update({'Location':
{'Old': current_location,
'New': location}})
if mode:
current_mode = __salt__[chassis_cmd]('get_general', cfg_tuning, mode_cmd)
if mode != current_mode:
ret['changes'].update({'Management Mode':
{'Old': current_mode,
'New': mode}})
if idrac_launch:
current_launch_method = __salt__[chassis_cmd]('get_general', cfg_tuning, launch_cmd)
if idrac_launch != current_launch_method:
ret['changes'].update({'iDrac Launch Method':
{'Old': current_launch_method,
'New': idrac_launch}})
if slot_names:
current_slot_names = __salt__[chassis_cmd]('list_slotnames')
for key, val in slot_names:
current_slot_name = current_slot_names.get(key).get('slotname')
if current_slot_name != val['name']:
old = {key: current_slot_name}
new = {key: val}
if ret['changes'].get('Slot Names') is None:
ret['changes'].update({'Slot Names':
{'Old': {},
'New': {}}})
ret['changes']['Slot Names']['Old'].update(old)
ret['changes']['Slot Names']['New'].update(new)
# TODO: Refactor this and make DRY - can probable farm this out to a new funciton
if blade_power_states:
# TODO: Get the power state list working
current_power_states = 'get a list of current power states'
for key, val in blade_power_states:
# TODO: Get the correct state infos
current_power_state = current_power_states.get(key).get('state')
# TODO: Don't just compare values, check if True should be "on" or "off" etc
if current_power_state != val:
old = {key: current_power_state}
new = {key: val}
if ret['changes'].get('Blade Power States') is None:
ret['changes'].update({'Blade Power States':
{'Old': {},
'New': {}}})
ret['changes']['Blade Power States']['Old'].update(old)
ret['changes']['Blade Power States']['New'].update(new)
if ret['changes'] == {}:
ret['comment'] = 'Dell chassis is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Dell chassis configuration will change.'
return ret
# Finally, set the necessary configurations on the chassis.
name = __salt__[chassis_cmd]('set_chassis_name', name)
if location:
location = __salt__[chassis_cmd]('set_chassis_location', location)
if mode:
mode = __salt__[chassis_cmd]('set_general', cfg_tuning, mode_cmd, mode)
if idrac_launch:
idrac_launch = __salt__[chassis_cmd]('set_general', cfg_tuning, launch_cmd, idrac_launch)
if slot_names:
slot_rets = []
for key, val in slot_names.iteritems():
slot_name = val.get('slotname')
slot_rets.append(__salt__[chassis_cmd]('set_slotname', key, slot_name))
if any(slot_rets) is False:
slot_names = False
else:
slot_names = True
if any([name, location, mode, idrac_launch, slot_names]) is False:
ret['result'] = False
ret['comment'] = 'There was an error setting the Dell chassis.'
ret['comment'] = 'Dell chassis was updated.'
return ret
def dell_switch(name, ip=None, netmask=None, gateway=None, dhcp=None,
password=None, snmp=None):
'''
Manage switches in a Dell Chassis.
name
The switch designation (e.g. switch-1, switch-2)
ip
The Static IP Address of the switch
netmask
The netmask for the static IP
gateway
The gateway for the static IP
dhcp
True: Enable DHCP
False: Do not change DHCP setup
(disabling DHCP is automatic when a static IP is set)
password
The access (root) password for the switch
snmp
The SNMP community string for the switch
Example:
.. code-block:: yaml
my-dell-chassis:
chassis.dell_switch:
- switch: switch-1
- ip: 192.168.1.1
- netmask: 255.255.255.0
- gateway: 192.168.1.254
- dhcp: True
- password: secret
- snmp: public
'''
ret = {'name': name,
'result': True,
'changes': {},
'comment': ''}
current_nic = __salt__['chassis.cmd']('network_info', module=name)
if current_nic.get('retcode', 0) != 0:
ret['result'] = False
ret['comment'] = current_nic['stdout']
return ret
if ip or netmask or gateway:
if not ip:
ip = current_nic['Network']['IP Address']
if not netmask:
ip = current_nic['Network']['Subnet Mask']
if not gateway:
ip = current_nic['Network']['Gateway']
if current_nic['Network']['DHCP Enabled'] == '0' and dhcp:
ret['changes'].update({'DHCP': {'Old': {'DHCP Enabled': current_nic['Network']['DHCP Enabled']},
'New': {'DHCP Enabled': dhcp}}})
if ((ip or netmask or gateway) and not dhcp and (ip != current_nic['Network']['IP Address'] or
netmask != current_nic['Network']['Subnet Mask'] or
gateway != current_nic['Network']['Gateway'])):
ret['changes'].update({'IP': {'Old': current_nic['Network'],
'New': {'IP Address': ip,
'Subnet Mask': netmask,
'Gateway': gateway}}})
if password:
if 'New' not in ret['changes']:
ret['changes']['New'] = {}
ret['changes']['New'].update({'Password': '*****'})
if snmp:
if 'New' not in ret['changes']:
ret['changes']['New'] = {}
ret['changes']['New'].update({'SNMP': '*****'})
if ret['changes'] == {}:
ret['comment'] = 'Switch ' + name + ' is already in desired state'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Switch ' + name + ' configuration will change'
return ret
# Finally, set the necessary configurations on the chassis.
dhcp_ret = net_ret = password_ret = snmp_ret = True
if dhcp:
dhcp_ret = __salt__['chassis.cmd']('set_niccfg', module=name, dhcp=dhcp)
if ip or netmask or gateway:
net_ret = __salt__['chassis.cmd']('set_niccfg', ip, netmask, gateway, module=name)
if password:
password_ret = __salt__['chassis.cmd']('deploy_password', 'root', password, module=name)
if snmp:
snmp_ret = __salt__['chassis.cmd']('deploy_snmp', password, module=name)
if any([password_ret, snmp_ret, net_ret, dhcp_ret]) is False:
ret['result'] = False
ret['comment'] = 'There was an error setting the switch {0}.'.format(name)
ret['comment'] = 'Dell chassis switch {0} was updated.'.format(name)
return ret