diff --git a/.travis.yml b/.travis.yml index 35b799ec18..84908d6f78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,15 +9,15 @@ branches: - develop before_install: - - sudo apt-get update && sudo apt-get install swig + - sudo apt-get update && sudo apt-get install swig supervisor - pip install http://dl.dropbox.com/u/174789/m2crypto-0.20.1.tar.gz - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi" install: pip install -r requirements.txt --use-mirrors -script: python setup.py test +script: sudo -E python setup.py test --runtests-opts='--run-destructive' notifications: irc: - channels: "irc.freenode.org#salt" + channels: "irc.freenode.org#salt-devel" on_success: change on_failure: change diff --git a/AUTHORS b/AUTHORS index 8991165584..64b58823f4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -40,10 +40,12 @@ Eric Poelke Erik Nolte Evan Borgstrom Jed Glazner +Jeff Bauer Jeffrey C. Ollie Jeff Schroeder Jonas Buckner Joseph Hall +Josmar Dias Kent Tenney Marc Abramowitz Markus Gattol @@ -56,6 +58,7 @@ Nathaniel Whiteinge Nigel Owen Pedro Algarvio Pierre Carrier +Rhys Elsmore Seth House Seth Vidal Thomas Schreiber diff --git a/HACKING.rst b/HACKING.rst index 26eac094c7..dd15ea37f3 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -63,6 +63,11 @@ Create a new `virtualenv`_:: virtualenv /path/to/your/virtualenv +.. note:: site packages + + If you wish to use installed packages rather than have pip download and + compile new ones into this environment, add "--system-site-packages". + .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv Activate the virtualenv:: @@ -75,11 +80,18 @@ Install Salt (and dependencies) into the virtualenv:: .. note:: Installing M2Crypto - If you and encounter the error ``command 'swig' failed with exit status 1`` + You may need ``swig`` and ``libssl-dev`` to build M2Crypto. If you + encounter the error ``command 'swig' failed with exit status 1`` while installing M2Crypto, try installing it with the following command:: env SWIG_FEATURES="-cpperraswarn -includeall -D__`uname -m`__ -I/usr/include/openssl" pip install M2Crypto + Debian and Ubuntu systems have modified openssl libraries and mandate that + a patched version of M2Crypto be installed. This means that M2Crypto + needs to be installed via apt: + + apt-get install python-m2crypto + Running a self-contained development version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -110,14 +122,21 @@ Edit the minion config file: "saltdev". This isn't strictly necessary but it will serve as a reminder of which Salt installation you are working with. +.. note:: Using `salt-call` with a :doc:`Standalone Minion ` + + If you plan to run `salt-call` with this self-contained development + environment in a masterless setup, you should invoke `salt-call` with + ``-c /path/to/your/virtualenv/etc/salt`` so that salt can find the minion + config file. Without the ``-c`` option, Salt finds its config files in `/etc/salt`. + Start the master and minion, accept the minon's key, and verify your local Salt installation is working:: - salt-master -c ./etc/salt/master -d - salt-minion -c ./etc/salt/minion -d - salt-key -c ./etc/salt/master -L - salt-key -c ./etc/salt/master -A - salt -c ./etc/salt/master '*' test.ping + salt-master -c ./etc/salt -d + salt-minion -c ./etc/salt -d + salt-key -c ./etc/salt -L + salt-key -c ./etc/salt -A + salt -c ./etc/salt '*' test.ping File descriptor limit ~~~~~~~~~~~~~~~~~~~~~ @@ -126,9 +145,11 @@ Check your file descriptor limit with:: ulimit -n -If it is less than 1024, you should increase it with:: +If it is less than 2047, you should increase it with:: + + ulimit -n 2047 + (or "limit descriptors 2047" for c-shell) - ulimit -n 1024 Running the tests ~~~~~~~~~~~~~~~~~ @@ -144,3 +165,7 @@ If you are on Python < 2.7 then you will also need unittest2:: Finally you use setup.py to run the tests with the following command:: ./setup.py test + +For greater control while running the tests, please try:: + + ./tests/runtests.py -h diff --git a/MANIFEST.in b/MANIFEST.in index 6a2831fc77..00fe361f78 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include AUTHORS +include HACKING.rst include LICENSE include README.rst include requirements.txt diff --git a/conf/master.template b/conf/master.template index 97508826d9..bb05812616 100644 --- a/conf/master.template +++ b/conf/master.template @@ -9,22 +9,50 @@ # The address of the interface to bind to #interface: 0.0.0.0 -# The port used by the publisher +# The tcp port used by the publisher #publish_port: 4505 -# The user to run salt +# Refresh the publisher connections when sending out commands, this is a fix +# for zeromq losing some minion connections. Default: True +#pub_refresh: True + +# The user to run the salt-master as. Salt will update all permissions to +# allow the specified user to run the master. If the modified files cause +# conflicts set verify_env to False. #user: root +# Max open files +# Each minion connecting to the master uses AT LEAST one file descriptor, the +# master subscription connection. If enough minions connect you might start +# seeing on the console(and then salt-master crashes): +# Too many open files (tcp_listener.cpp:335) +# Aborted (core dumped) +# +# By default this value will be the one of `ulimit -Hn`, ie, the hard limit for +# max open files. +# +# If you wish to set a different value than the default one, uncomment and +# configure this setting. Remember that this value CANNOT be higher than the +# hard limit. Raising the hard limit depends on your OS and/or distribution, +# a good way to find the limit is to search the internet for(for example): +# raise max open files hard limit debian +# +#max_open_files: 100000 + # The number of worker threads to start, these threads are used to manage # return calls made from minions to the master, if the master seems to be # running slowly, increase the number of threads #worker_threads: 5 -# The port used by the communication interface +# The port used by the communication interface. The ret (return) port is the +# interface used for the file server, authentication, job returnes, etc. #ret_port: 4506 +# Specify the location of the daemon process ID file +#pidfile: /var/run/salt-master.pid + # The root directory prepended to these options: pki_dir, cachedir, -# sock_dir, log_file, autosign_file. +# sock_dir, log_file, autosign_file, extension_modules #root_dir: / # Directory used to store public key data @@ -33,7 +61,10 @@ # Directory to store job and cache data #cachedir: /var/cache/salt -# Set the number of hours to keep old job information +# Verify and set permissions on configuration directories at startup +#verify_env: True + +# Set the number of hours to keep old job information in the job cache #keep_jobs: 24 # Set the default timeout for the salt command and api, the default is 5 @@ -41,7 +72,7 @@ #timeout: 5 # Set the directory used to hold unix sockets -#sock_dir: /tmp/salt-unix +#sock_dir: /var/run/salt # The master maintains a job cache, while this is a great addition it can be # a burden on the master for larger deployments (over 5000 minions). @@ -50,6 +81,9 @@ # #job_cache: True +# Cache minion grains and pillar data in the cachedir. +#minion_data_cache: True + # Set the acceptance level for serialization of messages. This should only be # set if the master is newer than 0.9.5 and the minion are older. This option # allows a 0.9.5 and newer master to communicate with minions 0.9.4 and @@ -100,6 +134,16 @@ # If an autosign_file is specified permissive access will allow group access # to that specific file. #permissive_pki_access: False +# +# Allow users on the master access to execute specific commands on minions. +# This setting should be treated with care since it opens up execution +# capabilities to non root users. By default this capability is completely +# disabled. +# +# client_acl: +# larry: +# - test.ping +# - network.* ##### Master Module Management ##### @@ -109,8 +153,9 @@ # Add any additional locations to look for master runners #runner_dirs: [] # -#Enable Cython for master side modules +# Enable Cython for master side modules #cython_enable: False +# ##### State System settings ##### ########################################## @@ -131,6 +176,17 @@ # The failhard option tells the minions to stop immediately after the first # failure detected in the state execution, defaults to False #failhard: False +# +# The state_verbose and state_output settings can be used to change the way +# state system data is printed to the display. By default all data is printed. +# The state_verbose setting can be set to True or False, when set to False +# all data that has a result of True and no changes will be suppressed. +#state_verbose: True +# +# The state_output setting changes if the output is the full multi line +# output for each changed state if set to 'full', but if set to 'terse' +# the output will be shortened to a single line. +#state_output: full ##### File Server settings ##### ########################################## @@ -180,7 +236,7 @@ # #ext_pillar: # - hiera: /etc/hiera.yaml -# - cmd: cat /etc/salt/yaml +# - cmd_yaml: cat /etc/salt/yaml # ##### Syndic settings ##### @@ -242,29 +298,11 @@ # - manage.up # -##### Cluster settings ##### -########################################## -# Salt supports automatic clustering, salt creates a single ip address which -# is shared among the individual salt components using ucarp. The private key -# and all of the minion keys are maintained across the defined cluster masters. -# The failover service is automatically managed via these settings - -# List the identifiers for the other cluster masters in this manner: -# [saltmaster-01.foo.com,saltmaster-02.foo.com,saltmaster-03.foo.com] -# The members of this master array must be running as salt minions to -# facilitate the distribution of cluster information -#cluster_masters: [] - -# The cluster modes are "paranoid" and "full" -# paranoid will only distribute the accepted minion public keys. -# full will also distribute the master private key. -#cluster_mode: paranoid - - ##### Logging settings ##### ########################################## # The location of the master log file #log_file: /var/log/salt/master +#key_logfile: /var/log/salt/key # # The level of messages to send to the log file. # One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. @@ -275,7 +313,7 @@ # The date and time format used in log messages. Allowed date/time formating # can be seen here: # http://docs.python.org/library/time.html#time.strftime -#log_datefmt: '%H:%M:%S' +#log_datefmt: '%Y-%m-%d %H:%M:%S' # # The format of the console logging messages. Allowed formatting options can # be seen here: diff --git a/conf/minion.template b/conf/minion.template index 09e0bcb250..6aac80ec43 100644 --- a/conf/minion.template +++ b/conf/minion.template @@ -16,6 +16,9 @@ # The user to run salt #user: root +# Specify the location of the daemon process ID file +#pidfile: /var/run/salt-minion.pid + # The root directory prepended to these options: pki_dir, cachedir, log_file. #root_dir: / @@ -34,6 +37,17 @@ # FQDN (for instance, Solaris). #append_domain: +# Custom static grains for this minion can be specified here and used in SLS +# files just like all other grains. This example sets 4 custom grains, with +# the 'roles' grain having two values that can be matched against: +#grains: +# roles: +# - webserver +# - memcache +# deployment: datacenter4 +# cabinet: 13 +# cab_u: 14-15 + # If the connection to the server is interrupted, the minion will # attempt to reconnect. sub_timeout allows you to control the rate # of reconnection attempts (in seconds). To disable reconnects, set @@ -43,22 +57,48 @@ # Where cache data goes #cachedir: /var/cache/salt +# Verify and set permissions on configuration directories at startup +#verify_env: True + # The minion can locally cache the return data from jobs sent to it, this # can be a good way to keep track of jobs the minion has executed # (on the minion side). By default this feature is disabled, to enable # set cache_jobs to True #cache_jobs: False +# set the directory used to hold unix sockets +#sock_dir: /var/run/salt + +# Backup files that are replaced by file.managed and file.recurse under +# 'cachedir'/file_backups relative to their original location and appended +# with a timestamp. The only valid setting is "minion". Disabled by default. +# +# Alternatively this can be specified for each file in state files: +# +# /etc/ssh/sshd_config: +# file.managed: +# - source: salt://ssh/sshd_config +# - backup: minion +# +#backup_mode: minion + # When waiting for a master to accept the minion's public key, salt will # continuously attempt to reconnect until successful. This is the time, in # seconds, between those reconnection attempts. -#acceptance_wait_time = 10 +#acceptance_wait_time: 10 # When healing a dns_check is run, this is to make sure that the originally # resolved dns has not changed, if this is something that does not happen in # your environment then set this value to False. #dns_check: True +# Windows platforms lack posix IPC and must rely on slower TCP based inter- +# process communications. Set ipc_mode to 'tcp' on such systems +#ipc_mode: ipc +# +# Overwrite the default tcp ports used by the minion when in tcp mode +#tcp_pub_port: 4510 +#tcp_pull_port: 4511 # The minion can include configuration from other files. To enable this, # pass a list of paths to this option. The paths can be either relative or @@ -69,12 +109,12 @@ # # # Include a config file from some other path: -#include: /etc/salt/extra_config +# include: /etc/salt/extra_config # # Include config from several files and directories: -#include: -# - /etc/salt/extra_config -# - /etc/roles/webserver +# include: +# - /etc/salt/extra_config +# - /etc/roles/webserver ##### Minion module management ##### ########################################## @@ -102,6 +142,7 @@ # # Enable Cython modules searching and loading. (Default: False) #cython_enable: False +# ##### State Management Settings ##### ########################################### @@ -119,6 +160,10 @@ # #renderer: yaml_jinja # +# The failhard option tells the minions to stop immediately after the first +# failure detected in the state execution, defaults to False +#failhard: False +# # state_verbose allows for the data returned from the minion to be more # verbose. Normally only states that fail or states that have changes are # returned, but setting state_verbose to True will return all states that @@ -145,6 +190,20 @@ # If using the local file directory, then the state top file name needs to be # defined, by default this is top.sls. #state_top: top.sls +# +# Run states when the minion daemon starts. To enable, set startup_states to: +# 'highstate' -- Execute state.highstate +# 'sls' -- Read in the sls_list option and execute the named sls files +# 'top' -- Read top_file option and execute based on that file on the Master +#startup_states: '' +# +# list of states to run when the minion starts up if startup_states is 'sls' +#sls_list: +# - edit.vim +# - hyper +# +# top file to execute if startup_states is 'top' +#top_file: '' ##### File Directory Settings ##### ########################################## @@ -204,6 +263,21 @@ # you've given access to. This is potentially quite insecure. #permissive_pki_access: False +# The state_verbose and state_output settings can be used to change the way +# state system data is printed to the display. By default all data is printed. +# The state_verbose setting can be set to True or False, when set to False +# all data that has a result of True and no changes will be suppressed. +#state_verbose: True +# +# The state_output setting changes if the output is the full multi line +# output for each changed state if set to 'full', but if set to 'terse' +# the output will be shortened to a single line. +#state_output: full +# +# Fingerprint of the master public key to double verify the master is valid, +# the master fingerprint can be found by running "salt-key -F master" on the +# salt master. +#master_finger: '' ###### Thread settings ##### ########################################### @@ -224,7 +298,7 @@ # # The date and time format used in log messages. Allowed date/time formating # can be seen on http://docs.python.org/library/time.html#time.strftime -#log_datefmt: '%H:%M:%S' +#log_datefmt: '%Y-%m-%d %H:%M:%S' # # The format of the console logging messages. Allowed formatting options can # be seen on http://docs.python.org/library/logging.html#logrecord-attributes @@ -249,6 +323,9 @@ # the module name is followed by a . and then the value. Also, all top level # data must be applied via the yaml dict construct, some examples: # +# You can specify that all modules should run in test mode: +#test: True +# # A simple value for the test module: #test.foo: foo # @@ -257,3 +334,16 @@ # # A dict for the test module: #test.baz: {spam: sausage, cheese: bread} + + +###### Update settings ###### +########################################### +# Using the features in Esky, a salt minion can both run as a frozen app and +# be updated on the fly. These options control how the update process +# (saltutil.update()) behaves. +# +# The url for finding and downloading updates. Disabled by default. +#update_url: False +# +# The list of services to restart after a successful update. Empty by default. +#update_restart_services: [] diff --git a/debian/changelog b/debian/changelog index 996feea66d..23cdc844e6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,16 @@ +salt (0.10.3) precise; urgency=low + + * New upstream version + + -- Thomas S Hatch Sun, 30 Aug 2012 13:34:10 -0700 + +salt (0.10.2) precise; urgency=low + + * Non-maintainer upload. + * New upstream version + + -- Dave Rawks Wed, 01 Aug 2012 13:34:10 -0700 + salt (0.9.9) precise; urgency=low * New upstream version diff --git a/debian/control b/debian/control index f412d714ea..05c161a3fa 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,8 @@ Build-Depends: debhelper (>= 8), python-m2crypto, python-zmq (>= 2.1.9), libzmq-dev (>= 2.1.9), - python-jinja2 + python-jinja2, + msgpack-python Standards-Version: 3.9.3 Homepage: http://saltstack.org #Vcs-Git: git://git.debian.org/collab-maint/salt.git diff --git a/debian/salt-master.upstart b/debian/salt-master.upstart index 27ebccff21..dbe09c306b 100644 --- a/debian/salt-master.upstart +++ b/debian/salt-master.upstart @@ -5,7 +5,7 @@ start on (net-device-up and runlevel [2345]) stop on runlevel [!2345] -expect daemon +respawn limit 10 5 respawn -exec /usr/bin/salt-master -d +exec /usr/bin/salt-master >/dev/null 2>&1 diff --git a/debian/salt-minion.upstart b/debian/salt-minion.upstart index c3ec5b43c1..8e85224ebd 100644 --- a/debian/salt-minion.upstart +++ b/debian/salt-minion.upstart @@ -6,7 +6,6 @@ start on (net-device-up stop on runlevel [!2345] respawn limit 10 5 +respawn -script - exec /usr/bin/python /usr/bin/salt-minion > /dev/null 2>&1 -end script +exec /usr/bin/salt-minion >/dev/null 2>&1 diff --git a/debian/salt-syndic.upstart b/debian/salt-syndic.upstart index 2eb9316294..11cea9cfb3 100644 --- a/debian/salt-syndic.upstart +++ b/debian/salt-syndic.upstart @@ -6,7 +6,6 @@ start on (net-device-up stop on runlevel [!2345] respawn limit 10 5 +respawn -script - exec /usr/bin/python /usr/bin/salt-syndic > /dev/null 2>&1 -end script +exec /usr/bin/salt-syndic >/dev/null 2>&1 diff --git a/doc/_templates/page.html b/doc/_templates/page.html index f8d09ebe8c..ce9a35d0ae 100644 --- a/doc/_templates/page.html +++ b/doc/_templates/page.html @@ -4,3 +4,24 @@
  • « SaltStack.org | 
  • Documentation home
  • {%- endblock %} + +{% block body %} +{{ super() }} +

    Comments

    +
    + +comments powered by Disqus +{% endblock %} + +{% block footer %} +{{ super() }} + +{% endblock %} diff --git a/doc/conf.py b/doc/conf.py index 7b9e053d8d..b0b968516c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -107,7 +107,7 @@ rst_prolog = """\ # A shortcut for linking to tickets on the GitHub issue tracker extlinks = { - 'blob': ('https://github.com/saltstack/salt/blob/v%s/%%s' % __version__, None), + 'blob': ('https://github.com/saltstack/salt/blob/%s/%%s' % 'develop', None), 'download': ('https://github.com/downloads/saltstack/salt/%s', None), 'issue': ('https://github.com/saltstack/salt/issues/%s', 'issue '), } diff --git a/doc/contents.rst b/doc/contents.rst index 9247308a98..5c07676c0f 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -24,7 +24,6 @@ Full Table of Contents topics/troubleshooting/index topics/troubleshooting/yaml_idiosyncrasies topics/community - topics/tutorials/standalone_minion topics/projects/index topics/event/index @@ -37,8 +36,11 @@ Full Table of Contents ref/states/all/index ref/renderers/* ref/renderers/all/index + ref/pillar/* + ref/pillar/all/index ref/runners ref/peer + ref/clientacl ref/syndic ref/python-api ref/file_server/index diff --git a/doc/index.rst b/doc/index.rst index 352f04fee4..0d0175631d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -201,6 +201,11 @@ Salt is many splendid things. Use Salt programmatically from your own scripts and programs easily and simply via ``import salt``. +:doc:`Automatic Updates and Frozen Deployments ` + Use a frozen install to make deployments easier (Even on Windows!). Or + take advantage of automatic updates to keep your minions running your + latest builds. + Reference --------- diff --git a/doc/man/salt-call.1 b/doc/man/salt-call.1 index 08d91110f1..89f18e5da9 100644 --- a/doc/man/salt-call.1 +++ b/doc/man/salt-call.1 @@ -1,4 +1,4 @@ -.TH "SALT-CALL" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-CALL" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-call \- salt-call Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .SH SYNOPSIS .sp @@ -37,6 +37,10 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] salt\-call [options] .ft P .fi +.SH DESCRIPTION +.sp +The salt\-call command is used to run module functions locally on a minion +instead of executing them from the master. .SH OPTIONS .INDENT 0.0 .TP @@ -105,4 +109,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt-cp.1 b/doc/man/salt-cp.1 index 17dc437717..6c1af38909 100644 --- a/doc/man/salt-cp.1 +++ b/doc/man/salt-cp.1 @@ -1,4 +1,4 @@ -.TH "SALT-CP" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-CP" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-cp \- salt-cp Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .sp Copy a file to a set of systems @@ -120,4 +120,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt-key.1 b/doc/man/salt-key.1 index eaa7718aad..1ec3c243f4 100644 --- a/doc/man/salt-key.1 +++ b/doc/man/salt-key.1 @@ -1,4 +1,4 @@ -.TH "SALT-KEY" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-KEY" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-key \- salt-key Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .SH SYNOPSIS .sp @@ -81,8 +81,8 @@ Delete the named minion key for command execution. .UNINDENT .INDENT 0.0 .TP -.B \-D DELETE_ALL, \-\-delete\-all=DELETE_ALL -Deleta all keys +.B \-D, \-\-delete\-all +Delete all keys .UNINDENT .INDENT 0.0 .TP @@ -91,9 +91,54 @@ The master configuration file needs to be read to determine where the Salt keys are stored via the pki_dir configuration value; default=/etc/salt/master .UNINDENT +.INDENT 0.0 +.TP +.B \-p PRINT, \-\-print=PRINT +Print the specified public key +.UNINDENT +.INDENT 0.0 +.TP +.B \-P, \-\-print\-all +Print all public keys +.UNINDENT +.INDENT 0.0 +.TP +.B \-q, \-\-quiet +Supress output +.UNINDENT +.INDENT 0.0 +.TP +.B \-y, \-\-yes +Answer \(aqYes\(aq to all questions presented, defaults to False +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-key\-logfile=KEY_LOGFILE +Send all output to a file. Default is /var/log/salt/key +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-gen\-keys=GEN_KEYS +Set a name to generate a keypair for use with salt +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-gen\-keys\-dir=GEN_KEYS_DIR +Set the directory to save the generated keypair. Only works +with \(aqgen_keys_dir\(aq option; default is the current directory. +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-keysize=KEYSIZE +Set the keysize for the generated key, only works with +the \(aq\-\-gen\-keys\(aq option, the key size must be 2048 or +higher, otherwise it will be rounded up to 2048. The +default is 2048. +.UNINDENT .SH AUTHOR Thomas S. Hatch and many others, please see the Authors file .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt-master.1 b/doc/man/salt-master.1 index 7b9ef22c96..33869b851b 100644 --- a/doc/man/salt-master.1 +++ b/doc/man/salt-master.1 @@ -1,4 +1,4 @@ -.TH "SALT-MASTER" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-MASTER" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-master \- salt-master Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .sp The Salt master daemon, used to control the Salt minions @@ -81,4 +81,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt-minion.1 b/doc/man/salt-minion.1 index a17654d30e..79be3ab9eb 100644 --- a/doc/man/salt-minion.1 +++ b/doc/man/salt-minion.1 @@ -1,4 +1,4 @@ -.TH "SALT-MINION" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-MINION" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-minion \- salt-minion Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .sp The Salt minion daemon, receives commands from a remote Salt master. @@ -82,4 +82,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt-run.1 b/doc/man/salt-run.1 index 214b9f625d..f020013a96 100644 --- a/doc/man/salt-run.1 +++ b/doc/man/salt-run.1 @@ -1,4 +1,4 @@ -.TH "SALT-RUN" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-RUN" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-run \- salt-run Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .sp Execute a Salt runner @@ -67,4 +67,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt-syndic.1 b/doc/man/salt-syndic.1 index 7d93ea1840..e14f50a045 100644 --- a/doc/man/salt-syndic.1 +++ b/doc/man/salt-syndic.1 @@ -1,4 +1,4 @@ -.TH "SALT-SYNDIC" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT-SYNDIC" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt-syndic \- salt-syndic Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .sp The Salt syndic daemon, a special minion that passes through commands from a @@ -76,4 +76,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt.1 b/doc/man/salt.1 index 360a9f714d..01f7447300 100644 --- a/doc/man/salt.1 +++ b/doc/man/salt.1 @@ -1,4 +1,4 @@ -.TH "SALT" "1" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT" "1" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt \- salt . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .SH SYNOPSIS .INDENT 0.0 @@ -145,7 +145,7 @@ file. .TP .B \-\-return Chose an alternative returner to call on the minion, if an alternative -returner is used then the return will not come back tot he command line +returner is used then the return will not come back to the command line but will be sent to the specified return system. .UNINDENT .INDENT 0.0 @@ -209,4 +209,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/man/salt.7 b/doc/man/salt.7 index 175723a856..026903dc71 100644 --- a/doc/man/salt.7 +++ b/doc/man/salt.7 @@ -1,4 +1,4 @@ -.TH "SALT" "7" "July 27, 2012" "0.10.2" "Salt" +.TH "SALT" "7" "September 30, 2012" "0.10.3" "Salt" .SH NAME salt \- Salt Documentation . @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructeredText. . .SH INTRODUCTION TO SALT We’re not just talking about NaCl..SS Distributed remote execution @@ -408,13 +408,39 @@ Salt can do. Depending on the primary way you want to manage your machines you may either want to visit the section regarding Salt States, or the section on Modules. .SS Debian +.SS Installation .sp Salt is currently available in in the Debian package tree: .sp \fI\%http://packages.debian.org/source/salt\fP .sp -If the desired Debian release is not supported rebuilding the source package -on your target platform or installing from source is recommended. +To install Salt on Wheezy or later use: +.sp +.nf +.ft C +sudo apt\-get install salt\-master +sudo apt\-get install salt\-minion +.ft P +.fi +.SS Squeeze +.sp +Salt is available for squeeze in the Debian backports repository. For more +information how to use debian\-backports see +\fI\%http://backports-master.debian.org/Instructions/\fP +.sp +.nf +.ft C +cat < at version 0.10.2 of Salt. It has mainly been tested on Solaris 10 (sparc), though it is built for, and should work fine on Solaris 10 (x86), Solaris 9 (sparc/x86) and 11 (sparc/x86) also. Most of the testing has also just focused on the minion, though it has verified that the master starts up successfully on Solaris 10. +.sp +Comments and patches for better support on these platforms is very welcome. Currently at version 0.10.2 of salt, grain detection is weak but patches that very much improve the grain detection will be released in 0.10.3. Work is also underway to include support for services and packages in Solaris. +.sp +Salt is dependent on the following additional packages. These will automatically be installed as +dependencies of the \fBpy_salt\fP package. +.sp +.nf +.ft C +py_yaml +py_pyzmq +py_jinja2 +py_msgpack_python +py_m2crypto +py_crypto +python +.ft P +.fi +.SS Installation +.sp +To install Salt from the OpenCSW package repository you first need to install \fI\%pkgutil\fP assuming you don\(aqt already have it installed: +.sp +On Solaris 10: +.sp +.nf +.ft C +pkgadd \-d http://get.opencsw.org/now +.ft P +.fi +.sp +On Solaris 9: +.sp +.nf +.ft C +wget http://mirror.opencsw.org/opencsw/pkgutil.pkg +pkgadd \-d pkgutil.pkg all +.ft P +.fi +.sp +Once pkgutil is installed you\(aqll need to edit it\(aqs config file \fB/etc/opt/csw/pkgutil.conf\fP to point it at the unstable catalog: +.sp +.nf +.ft C +\- #mirror=http://mirror.opencsw.org/opencsw/testing ++ mirror=http://mirror.opencsw.org/opencsw/unstable +.ft P +.fi +.sp +Ok, time to install salt. +.sp +.nf +.ft C +# Update the catalog +root> /opt/csw/bin/pkgutil \-U +# Install salt +root> /opt/csw/bin/pkgutil \-i \-y py_salt +.ft P +.fi +.SS Minion Configuration +.sp +Now that salt is installed you can find it\(aqs configuration files in: +.sp +\fB/etc/opt/csw/salt/\fP +.sp +You\(aqll want to edit the minion config file to set the name of your salt master server: +.sp +.nf +.ft C +\- #master: salt ++ master: your\-salt\-server +.ft P +.fi +.sp +You can now start the salt minion like so: +.sp +On Solaris 10: +.sp +.nf +.ft C +svcadm enable salt\-minion +.ft P +.fi +.sp +On Solaris 9: +.sp +.nf +.ft C +/etc/init.d/salt\-minion start +.ft P +.fi +.sp +You should now be able to log onto the salt master and check to see if the salt\-minion key is awaiting acceptance: +.sp +.nf +.ft C +salt\-key \-l un +.ft P +.fi +.sp +Accept the key: +.sp +.nf +.ft C +salt\-key \-a +.ft P +.fi +.sp +Run a simple test against the minion: +.sp +.nf +.ft C +salt \(aq\(aq test.ping +.ft P +.fi +.SS Troubleshooting +.sp +Logs are in \fB/var/log/salt\fP .SH DEVELOPING SALT .sp If you want to help develop Salt there is a great need and your patches are @@ -1283,6 +1428,12 @@ Create a new \fI\%virtualenv\fP: virtualenv /path/to/your/virtualenv .ft P .fi +.IP Note +site packages +.sp +If you wish to use installed packages rather than have pip download and +compile new ones into this environment, add "\-\-system\-site\-packages". +.RE .sp Activate the virtualenv: .sp @@ -1302,7 +1453,8 @@ pip install \-e ./salt # the path to the salt git clone from above .IP Note Installing M2Crypto .sp -If you and encounter the error \fBcommand \(aqswig\(aq failed with exit status 1\fP +You may need \fBswig\fP and \fBlibssl\-dev\fP to build M2Crypto. If you +encounter the error \fBcommand \(aqswig\(aq failed with exit status 1\fP while installing M2Crypto, try installing it with the following command: .sp .nf @@ -1310,6 +1462,15 @@ while installing M2Crypto, try installing it with the following command: env SWIG_FEATURES="\-cpperraswarn \-includeall \-D__\(gauname \-m\(ga__ \-I/usr/include/openssl" pip install M2Crypto .ft P .fi +.sp +Debian and Ubuntu systems have modified openssl libraries and mandate that +a patched version of M2Crypto be installed. This means that M2Crypto +needs to be installed via apt: +.INDENT 0.0 +.INDENT 3.5 +apt\-get install python\-m2crypto +.UNINDENT +.UNINDENT .RE .SS Running a self\-contained development version .sp @@ -1351,17 +1512,25 @@ Uncomment and change the \fBid:\fP value to something descriptive like "saltdev". This isn\(aqt strictly necessary but it will serve as a reminder of which Salt installation you are working with. .UNINDENT +.IP Note +Using \fIsalt\-call\fP with a \fBStandalone Minion\fP +.sp +If you plan to run \fIsalt\-call\fP with this self\-contained development +environment in a masterless setup, you should invoke \fIsalt\-call\fP with +\fB\-c /path/to/your/virtualenv/etc/salt\fP so that salt can find the minion +config file. Without the \fB\-c\fP option, Salt finds its config files in \fI/etc/salt\fP. +.RE .sp Start the master and minion, accept the minon\(aqs key, and verify your local Salt installation is working: .sp .nf .ft C -salt\-master \-c ./etc/salt/master \-d -salt\-minion \-c ./etc/salt/minion \-d -salt\-key \-c ./etc/salt/master \-L -salt\-key \-c ./etc/salt/master \-A -salt \-c ./etc/salt/master \(aq*\(aq test.ping +salt\-master \-c ./etc/salt \-d +salt\-minion \-c ./etc/salt \-d +salt\-key \-c ./etc/salt \-L +salt\-key \-c ./etc/salt \-A +salt \-c ./etc/salt \(aq*\(aq test.ping .ft P .fi .SS File descriptor limit @@ -1374,11 +1543,12 @@ ulimit \-n .ft P .fi .sp -If it is less than 1024, you should increase it with: +If it is less than 2047, you should increase it with: .sp .nf .ft C -ulimit \-n 1024 +ulimit \-n 2047 +(or "limit descriptors 2047" for c\-shell) .ft P .fi .SS Running the tests @@ -1406,6 +1576,14 @@ Finally you use setup.py to run the tests with the following command: \&./setup.py test .ft P .fi +.sp +For greater control while running the tests, please try: +.sp +.nf +.ft C +\&./tests/runtests.py \-h +.ft P +.fi .SH CONFIGURING SALT .sp Salt configuration is very simple. The default configuration for the @@ -1704,7 +1882,7 @@ what the grain is and remember that grains need to be static data. The core module in the grains package is where the main grains are loaded by the Salt minion and provides the principal example of how to write grains: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/grains/core.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/grains/core.py\fP .SS Syncing Grains .sp Syncing grains can be done a number of ways, they are automatically synced when @@ -1718,6 +1896,11 @@ A predefined group of minions declared in the master configuration file \fBnodegroups\fP setting as a compound target. .UNINDENT .sp +Nodegroups are declared using a compound target specification. The compount +target documentation can be found here: +.sp +\fBCompound Matchers\fP +.sp For example, in the master config file \fBnodegroups\fP setting: .sp .nf @@ -1865,9 +2048,9 @@ additional minion, so that the job is constantly running on 10 minions. following the \fBinstallation\fP and the \fBconfiguration\fP instructions. .IP "Stuck?" .sp -If you get stuck at any point, there are many ways to \fBget help from -the Salt community\fP including our mailing list and our -IRC channel. +There are many ways to \fBget help from the Salt community\fP including our +\fI\%mailing list\fP +and our \fI\%IRC channel\fP #salt. .RE .SS Order your minions around .sp @@ -2099,8 +2282,8 @@ this: .sp .nf .ft C -/apache/init.sls -/apache/httpd.conf +apache/init.sls +apache/httpd.conf .ft P .fi .sp @@ -2110,7 +2293,7 @@ directly. But with more than a single SLS file, more components can be added to the toolkit, consider this SSH example: .sp -\fB/ssh/init.sls:\fP +\fBssh/init.sls:\fP .sp .nf .ft C @@ -2171,13 +2354,13 @@ Now our State Tree looks like this: .sp .nf .ft C -/apache/init.sls -/apache/httpd.conf -/ssh/init.sls -/ssh/server.sls -/ssh/banner -/ssh/ssh_config -/ssh/sshd_config +apache/init.sls +apache/httpd.conf +ssh/init.sls +ssh/server.sls +ssh/banner +ssh/ssh_config +ssh/sshd_config .ft P .fi .sp @@ -2196,7 +2379,7 @@ needs to be placed. .sp These examples will add more watchers to apache and change the ssh banner. .sp -\fB/ssh/custom\-server.sls:\fP +\fBssh/custom\-server.sls:\fP .sp .nf .ft C @@ -2210,7 +2393,7 @@ These examples will add more watchers to apache and change the ssh banner. .ft P .fi .sp -\fB/python/mod_python.sls:\fP +\fBpython/mod_python.sls:\fP .sp .nf .ft C @@ -2234,10 +2417,11 @@ to configure the banner. .sp In the new mod_python SLS the mod_python package is added, but more importantly the apache service was extended to also watch the mod_python package. +.IP "Using extend with require or watch" .sp -There is a bit of a trick here, in the extend statement Requisite Statements -are extended, so the \fB\- pkg: mod_python\fP is appended to the watch list. But -all other statements are overwritten. +The \fBextend\fP statement works differently for \fBrequire\fP or \fBwatch\fP. +It appends to, rather than replacing the requisite component. +.RE .SS Understanding the Render System .sp Since the SLS data is just plain old data, it does not need to be represented @@ -2267,7 +2451,7 @@ available, \fBsalt\fP, \fBgrains\fP, and \fBpillar\fP. The \fBsalt\fP object all any Salt function to be called from within the template, and \fBgrains\fP allows for the Grains to be accessed from within the template. A few examples: .sp -\fB/apache/init.sls:\fP +\fBapache/init.sls:\fP .sp .nf .ft C @@ -2311,7 +2495,7 @@ Red Hat, then the name of the Apache package and service needs to be httpd. A more aggressive way to use Jinja can be found here, in a module to set up a MooseFS distributed filesystem chunkserver: .sp -\fB/moosefs/chunk.sls:\fP +\fBmoosefs/chunk.sls:\fP .sp .nf .ft C @@ -2382,7 +2566,7 @@ but a SLS file set to use another renderer can be easily added to the tree. .sp This example shows a very basic Python SLS file: .sp -\fB/python/django.sls:\fP +\fBpython/django.sls:\fP .sp .nf .ft C @@ -2420,14 +2604,14 @@ decision as the default, but that unbridled power can be obtained where needed by using a pure Python SLS. .SS Running and debugging salt states. .sp -after writing out your top.sls file, to run it you call -\fBsalt \(aq*\(aq state.highstate\fP. If you get back just the hostnames with -a : after, but no return, then chances are there is a problem with the sls -files. To debug these, to see what\(aqs going on, and see the errors, use the -\fBsalt\-call\fP command like so: \fBsalt\-call state.highstate \-l debug\fP. This -should help you figure out what\(aqs going wrong. You can also start the minions -in the foreground in debug mode, as a possible way to help debug as well. -To start the minion in debug mode call it like this: \fBsalt\-minion \-l debug\fP. +Once the rules in an SLS are ready, they need to be tested to ensure they +work properly. To invoke the rules, simply execute \fBsalt \(aq*\(aq state.highstate\fP +on the command line. If you get back just the hostnames with a \fI:\fP after, +but no return, chances are there is a problem with the one or more of the sls +files. Use the \fBsalt\-call\fP command: \fBsalt\-call state.highstate \-l debug\fP +and examine the output for errors. This should help troubleshoot the issue. +The minions can also be started in the foreground in debug mode. Start the +minion in debug mode with: \fBsalt\-minion \-l debug\fP. .sp Now onto the \fBStates tutorial, part 1\fP. .SH STATES TUTORIAL, PART 1 @@ -2443,9 +2627,9 @@ Apache HTTP server and to ensure the server is running. following the \fBinstallation\fP and the \fBconfiguration\fP instructions. .IP "Stuck?" .sp -If you get stuck at any point, there are many ways to \fBget help from -the Salt community\fP including our mailing list and our -IRC channel. +There are many ways to \fBget help from the Salt community\fP including our +\fI\%mailing list\fP +and our \fI\%IRC channel\fP #salt. .RE .SS Setting up the Salt State Tree .sp @@ -2612,13 +2796,14 @@ This tutorial focused on getting a simple Salt States configuration working. \fBPart 2\fP will build on this example to cover more advanced \fIsls\fP syntax and will explore more of the states that ship with Salt. .SH STATES TUTORIAL, PART 2 +.IP Note +This tutorial builds on the topic covered in \fBpart 1\fP. +It is recommended that you begin there. +.RE .sp -This tutorial builds on the topic covered in \fBpart 1\fP. It is -recommended that you begin there. -.sp -In the last Salt States tutorial we covered the basics of installing a package. -In this tutorial we will modify our \fBwebserver.sls\fP file to be more -complicated, have requirements, and use even more Salt States. +In the \fBlast part\fP of the Salt States tutorial we covered +the basics of installing a package. We will now modify our \fBwebserver.sls\fP +file to have requirements, and use even more Salt States. .SS Call multiple States .sp You can specify multiple \fIstate declarations\fP under @@ -2768,25 +2953,25 @@ explained in \fBPart 3\fP. .SS Next steps .sp In \fBpart 3\fP we will discuss how to use includes, extends and -templating to make hugely complicated State Tree configurations dead\-simple. +templating to make a more complete State Tree configuration. .SH STATES TUTORIAL, PART 3 +.IP Note +This tutorial builds on the topic covered in \fBpart1\fP and +\fBpart 2\fP. It is recommended that you begin there. +.RE .sp -This tutorial builds on the topic covered in \fBpart 2\fP. It is -recommended that you begin there. -.sp -This tutorial will cover more advanced templating and configuration techniques -for \fBsls\fP files. +This part of the tutorial will cover more advanced templating and +configuration techniques for \fBsls\fP files. .SS Templating SLS modules .sp -SLS modules may require programming logic or inline executions. This is +SLS modules may require programming logic or inline execution. This is accomplished with module templating. The default module templating system used is \fI\%Jinja2\fP and may be configured by changing the \fBrenderer\fP value in the master config. .sp -All states are passed through a templating system when they are initially read, -so all that is required to make use of the templating system is to add some -templating code. An example of an sls module with templating may look like -this: +All states are passed through a templating system when they are initially read. +To make use of the templating system, simple add some templating markup. +An example of an sls module with templating markup may look like this: .sp .nf .ft C @@ -2812,8 +2997,8 @@ curly: .SS Using Grains in SLS modules .sp Often times a state will need to behave differently on different systems. -\fBSalt grains\fP can be used from within sls modules. An object -called \fBgrains\fP is made available in the template context: +\fBSalt grains\fP objects are made available +in the template context. The \fIgrains\fP can be used from within sls modules: .sp .nf .ft C @@ -2853,22 +3038,23 @@ The Salt module functions are also made available in the template context as .sp Below is an example that uses the \fBnetwork.hwaddr\fP function to retrieve the MAC address for eth0: -.INDENT 0.0 -.INDENT 3.5 +.sp +.nf +.ft C salt[\(aqnetwork.hwaddr\(aq](\(aqeth0\(aq) -.UNINDENT -.UNINDENT +.ft P +.fi .SS Advanced SLS module syntax .sp -Last we will cover some incredibly useful techniques for more complex State +Lastly, we will cover some incredibly useful techniques for more complex State trees. .SS \fIInclude declaration\fP .sp -You have seen an example of how to spread a Salt tree across several files but -in order to be able to have \fIrequisite references\fP -span multiple files you must use an \fIinclude declaration\fP. For example: +A previous example showed how to spread a Salt tree across several files. +Similarly, \fIrequisite references\fP span multiple +files by using an \fIinclude declaration\fP. For example: .sp -\fBpython\-libs.sls\fP: +\fBpython/python\-libs.sls\fP: .sp .nf .ft C @@ -2877,7 +3063,7 @@ python\-dateutil: .ft P .fi .sp -\fBdjango.sls\fP: +\fBpython/django.sls\fP: .sp .nf .ft C @@ -2896,7 +3082,7 @@ You can modify previous declarations by using an \fIextend declaration\fP. For example the following modifies the Apache tree to also restart Apache when the vhosts file is changed: .sp -\fBapache.sls\fP: +\fBapache/apache.sls\fP: .sp .nf .ft C @@ -2905,7 +3091,7 @@ apache: .ft P .fi .sp -\fBmywebsite.sls\fP: +\fBapache/mywebsite.sls\fP: .sp .nf .ft C @@ -2920,16 +3106,21 @@ extend: /etc/httpd/extra/httpd\-vhosts.conf: file.managed: - \- source: salt://httpd\-vhosts.conf + \- source: salt://apache/httpd\-vhosts.conf .ft P .fi +.IP "Using extend with require or watch" +.sp +The \fBextend\fP statement works differently for \fBrequire\fP or \fBwatch\fP. +It appends to, rather than replacing the requisite component. +.RE .SS \fIName declaration\fP .sp You can override the \fIID declaration\fP by using a \fIname declaration\fP. For example, the previous example is a bit more maintainable if rewritten as follows: .sp -\fBmywebsite.sls\fP: +\fBapache/mywebsite.sls\fP: .sp .nf .ft C @@ -2945,7 +3136,7 @@ extend: mywebsite: file.managed: \- name: /etc/httpd/extra/httpd\-vhosts.conf - \- source: salt://httpd\-vhosts.conf + \- source: salt://apache/httpd\-vhosts.conf .ft P .fi .SS \fINames declaration\fP @@ -3290,8 +3481,7 @@ Salt file server the \fBpillar_roots\fP option in the master config is based on environments mapping to directories. The pillar data is then mapped to minions based on matchers in a top file which is laid out in the same way as the state top file. Salt pillars can use the same matcher types as the -standard top file, and if you are having difficulty matching a specific minion -in your pillar top file, you may want to specify PCRE matching. +standard top file. .sp The configuration for the \fBpillar_roots\fP in the master config is identical in behavior and function as the \fBfile_roots\fP configuration: @@ -3315,25 +3505,6 @@ used for States, and has the same structure: base: \(aq*\(aq: \- packages - \(aqsomeminion\(aq: - \- someminion\-specials -.ft P -.fi -.sp -This simple pillar top file declares that information for all minions can be -found in the \fBpackages.sls\fP file [1], while -\fBsomeminion\-specials.sls\fP contains overriding or additional information just -for one special minion. -.sp -.nf -.ft C -base: - \(aq.*\(aq: - \- match: pcre - \- packages - \(aq(someminion|anotherminion)\(aq: - \- match: pcre - \- someminion\-specials .ft P .fi .sp @@ -3356,14 +3527,6 @@ somekey: globalvalue .ft P .fi .sp -\fB/srv/pillar/someminion\-specials.sls\fP -.sp -.nf -.ft C -somekey: specialvalue -.ft P -.fi -.sp Now this data can be used from within modules, renderers, State SLS files, and more via the shared pillar \fI\%dict\fP: .sp @@ -3419,6 +3582,16 @@ on \(aqsomeminion\(aq: somekey has value: {{ pillar[\(aqsomekey\(aq] }} .ft P .fi +.SS Viewing Minion Pillar +.sp +Once the pillar is set up the data can be viewed on the minion via the +\fBpillar.data\fP module: +.sp +.nf +.ft C +# salt \(aq*\(aq pillar.data +.ft P +.fi .SS Footnotes .IP [1] 5 Note that you cannot just list key/value\-information in \fBtop.sls\fP. @@ -3714,6 +3887,31 @@ needs to be run with the \fBpython26\fP executable. An extensive list of \fBYAML idiosyncrasies\fP has been compiled. +.SS Live Python Debug Output +.sp +If the minion or master seems to be unresponsive, a SIGUSR1 can be passed to +the processes to display where in the code they are running. If encountering a +situation like this, this debug information can be invaluable. First make +sure the master of minion are running in the foreground: +.sp +.nf +.ft C +# salt\-master \-l debug +# salt\-minion \-l debug +.ft P +.fi +.sp +The pass the signal to the master or minion when it seems to be unresponsive: +.sp +.nf +.ft C +killall \-SIGUSR1 salt\-master +killall \-SIGUSR1 salt\-minion +.ft P +.fi +.sp +When filing an issue or sending questions to the mailing list for a problem +with an unresponsive daemon this information can be invaluable. .SH YAML IDIOSYNCRASIES .sp One of Salt\(aqs strengths, the use of existing serialization systems for @@ -3783,7 +3981,7 @@ is not desirable, then a deeply nested dict can be declared with curly braces: .fi .SS Integers are Parsed as Integers .sp -NOTE: This has been fixed in salt 0.9.10, as of this release passing an +NOTE: This has been fixed in salt 0.10.0, as of this release passing an integer that is preceded by a 0 will be correctly parsed .sp When passing \fI\%integers\fP into an SLS file, they are passed as integers. This means @@ -3806,7 +4004,7 @@ This is best explained when setting the mode for a file: .sp Salt manages this well, since the mode is passed as 644, but if the mode is zero padded as 0644, then it is read by YAML as an integer and evaluated as -a hexadecimal value, 0644 becomes 420. Therefore, if the file mode is +an octal value, 0644 becomes 420. Therefore, if the file mode is preceded by a 0 then it needs to be passed as a string: .sp .nf @@ -3885,6 +4083,36 @@ fred: \- enc: dsa .ft P .fi +.SS YAML support only plain ASCII +.sp +According to YAML specification, only ASCII characters can be used. +.sp +Within double\-quotes, special characters may be represented with C\-style +escape sequences starting with a backslash ( \e ). +.sp +Examples: +.sp +.nf +.ft C +\- micro: "\eu00b5" +\- copyright: "\eu00A9" +\- A: "\ex41" +\- alpha: "\eu0251" +\- Alef: "\eu05d0" +.ft P +.fi +.sp +List of useable \fI\%Unicode characters\fP will help you to identify correct numbers. +.sp +Python can also be used to discover the Unicode number for a character: +.sp +.nf +.ft C +repr(u"Text with wrong characters i need to figure out") +.ft P +.fi +.sp +This shell command can find wrong characters in your SLS files: .SH COMMUNITY .sp Join the Salt! @@ -3905,6 +4133,10 @@ The \fB#salt\fP IRC channel is hosted on the popular \fI\%Freenode\fP network. Y can use the \fI\%Freenode webchat client\fP right from your browser. .sp \fI\%Logs of the IRC channel activity\fP are being collected courtesy of Moritz Lenz. +.SS Salt development +.sp +If you wish to discuss the development of Salt itself join us in +\fB#salt\-devel\fP. .SS Follow on Github .sp The Salt code is developed via Github. Follow Salt for constant updates on what @@ -3932,6 +4164,10 @@ A few examples of salt states from the community: \fI\%https://github.com/uggedal/states\fP .IP \(bu 2 \fI\%https://github.com/mattmcclean/salt-openstack/tree/master/salt\fP +.IP \(bu 2 +\fI\%https://github.com/rentalita/ubuntu-setup/\fP +.IP \(bu 2 +\fI\%https://github.com/brutasse/states\fP .UNINDENT .SS Follow on ohloh .sp @@ -4004,6 +4240,12 @@ Create a new \fI\%virtualenv\fP: virtualenv /path/to/your/virtualenv .ft P .fi +.IP Note +site packages +.sp +If you wish to use installed packages rather than have pip download and +compile new ones into this environment, add "\-\-system\-site\-packages". +.RE .sp Activate the virtualenv: .sp @@ -4023,7 +4265,8 @@ pip install \-e ./salt # the path to the salt git clone from above .IP Note Installing M2Crypto .sp -If you and encounter the error \fBcommand \(aqswig\(aq failed with exit status 1\fP +You may need \fBswig\fP and \fBlibssl\-dev\fP to build M2Crypto. If you +encounter the error \fBcommand \(aqswig\(aq failed with exit status 1\fP while installing M2Crypto, try installing it with the following command: .sp .nf @@ -4031,6 +4274,15 @@ while installing M2Crypto, try installing it with the following command: env SWIG_FEATURES="\-cpperraswarn \-includeall \-D__\(gauname \-m\(ga__ \-I/usr/include/openssl" pip install M2Crypto .ft P .fi +.sp +Debian and Ubuntu systems have modified openssl libraries and mandate that +a patched version of M2Crypto be installed. This means that M2Crypto +needs to be installed via apt: +.INDENT 0.0 +.INDENT 3.5 +apt\-get install python\-m2crypto +.UNINDENT +.UNINDENT .RE .SS Running a self\-contained development version .sp @@ -4072,17 +4324,25 @@ Uncomment and change the \fBid:\fP value to something descriptive like "saltdev". This isn\(aqt strictly necessary but it will serve as a reminder of which Salt installation you are working with. .UNINDENT +.IP Note +Using \fIsalt\-call\fP with a \fBStandalone Minion\fP +.sp +If you plan to run \fIsalt\-call\fP with this self\-contained development +environment in a masterless setup, you should invoke \fIsalt\-call\fP with +\fB\-c /path/to/your/virtualenv/etc/salt\fP so that salt can find the minion +config file. Without the \fB\-c\fP option, Salt finds its config files in \fI/etc/salt\fP. +.RE .sp Start the master and minion, accept the minon\(aqs key, and verify your local Salt installation is working: .sp .nf .ft C -salt\-master \-c ./etc/salt/master \-d -salt\-minion \-c ./etc/salt/minion \-d -salt\-key \-c ./etc/salt/master \-L -salt\-key \-c ./etc/salt/master \-A -salt \-c ./etc/salt/master \(aq*\(aq test.ping +salt\-master \-c ./etc/salt \-d +salt\-minion \-c ./etc/salt \-d +salt\-key \-c ./etc/salt \-L +salt\-key \-c ./etc/salt \-A +salt \-c ./etc/salt \(aq*\(aq test.ping .ft P .fi .SS File descriptor limit @@ -4095,11 +4355,12 @@ ulimit \-n .ft P .fi .sp -If it is less than 1024, you should increase it with: +If it is less than 2047, you should increase it with: .sp .nf .ft C -ulimit \-n 1024 +ulimit \-n 2047 +(or "limit descriptors 2047" for c\-shell) .ft P .fi .SS Running the tests @@ -4127,6 +4388,14 @@ Finally you use setup.py to run the tests with the following command: \&./setup.py test .ft P .fi +.sp +For greater control while running the tests, please try: +.sp +.nf +.ft C +\&./tests/runtests.py \-h +.ft P +.fi .SH SALT BASED PROJECTS .sp A number of unofficial open source projects, based on Salt, or written to @@ -4172,7 +4441,7 @@ by the same system user that Salt is running as. To listen to events a SaltEvent object needs to be created and then the get_event function needs to be run. The SaltEvent object needs to know the location that the Salt unix sockets are kept. In the configuration this is the \fBsock_dir\fP option. The -\fBsock_dir\fP option defaults to "/tmp/.salt\-unix" on most systems. +\fBsock_dir\fP option defaults to "/var/run/salt" on most systems. .sp The following code will check for a single event: .sp @@ -4180,7 +4449,7 @@ The following code will check for a single event: .ft C import salt.utils.event -event = salt.utils.event.MasterEvent(\(aq/tmp/.salt\-unix\(aq) +event = salt.utils.event.MasterEvent(\(aq/var/run/salt\(aq) data = event.get_event() .ft P @@ -4197,23 +4466,23 @@ instead of the default 5. .ft C import salt.utils.event -event = salt.utils.event.MasterEvent(\(aq/tmp/.salt\-unix\(aq) +event = salt.utils.event.MasterEvent(\(aq/var/run/salt\(aq) data = event.get_event(wait=10, tag=\(aqauth\(aq) .ft P .fi .sp -Instead of looking for a single event, the iter_event method can be used to -make a generator which will continually yield salt events. The iter_event +Instead of looking for a single event, the iter_events method can be used to +make a generator which will continually yield salt events. The iter_events method also accepts a tag, but not a wait time: .sp .nf .ft C import salt.utils.event -event = salt.utils.event.MasterEvent(\(aq/tmp/.salt\-unix\(aq) +event = salt.utils.event.MasterEvent(\(aq/var/run/salt\(aq) -for data in event.iter_event(tag=\(aqauth\(aq): +for data in event.iter_events(tag=\(aqauth\(aq): print(data) .ft P .fi @@ -4246,7 +4515,7 @@ executions to manipulating the flow of how data is handled by Salt. The minion execution modules or just \fBmodules\fP are the core to what Salt is and does. These modules are found in: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules\fP .sp These modules are what is called by the Salt command line and the salt client API. Adding modules is done by simply adding additional Python modules to the @@ -4263,7 +4532,7 @@ of execution modules and types to specific Salt minions. .sp The code used to generate the Salt grains can be found here: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/grains\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/grains\fP .SS States .sp Salt supports state enforcement, this makes Salt a high speed and very efficient @@ -4271,7 +4540,7 @@ solution for system configuration management. .sp States can be easily added to Salt by dropping a new state module in: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/states\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/states\fP .SS Renderers .sp Salt states are controlled by simple data structures, these structures can be @@ -4282,7 +4551,7 @@ it. .sp The existing renderers can be found here: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/renderers\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/renderers\fP .SS Returners .sp The Salt commands all produce a return value, that return value is sent to the @@ -4292,7 +4561,7 @@ from an SQL or NoSQL database, to a custom application made to use Salt. .sp The existing returners can be found here: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/returners\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/returners\fP .SS Runners .sp Sometimes a certain application can be made to execute and run from the @@ -4302,7 +4571,7 @@ act as a generic interface for encapsulating master side executions. .sp Existing Salt runners are located here: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/runners\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/runners\fP .SH MODULES .sp Salt modules are the functions called by the \fBsalt\fP command. @@ -4439,13 +4708,13 @@ regardless of what the actual module is named. .sp The package manager modules are the best example of using the \fB__virtual__\fP function: -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules/pacman.py\fP -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules/yumpkg.py\fP -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules/apt.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules/pacman.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules/yumpkg.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules/apt.py\fP .SS Documentation .sp Salt modules are self documenting, the \fBsys.doc()\fP function will return the -documentation for all available Facter modules: +documentation for all available modules: .sp .nf .ft C @@ -4516,7 +4785,7 @@ functions for Salt, but to stand as examples for building out more Salt modules. .sp The existing modules can be found here: -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules\fP .sp The most simple module is the test module, it contains the simplest Salt function, \fBtest.ping\fP: @@ -4629,6 +4898,12 @@ Manages configuration files via augeas T} _ T{ +\fBbluez\fP +T} T{ +Support for Bluetooth (using Bluez in Linux) +T} +_ +T{ \fBbrew\fP T} T{ T} @@ -4640,6 +4915,18 @@ Specialized routines used by the butter cloud component T} _ T{ +\fBca\fP +T} T{ +A salt interface for running a Certificate Authority (CA) +T} +_ +T{ +\fBcassandra\fP +T} T{ +Cassandra NoSQL Database Module +T} +_ +T{ \fBcluster\fP T} T{ The cluster module is used to distribute and activate salt HA cluster @@ -4690,7 +4977,6 @@ _ T{ \fBdjango\fP T} T{ -Manage Django sites T} _ T{ @@ -4700,6 +4986,12 @@ Support for Portage T} _ T{ +\fBevent\fP +T} T{ +Fire events on the minion, events can be fired up to the master +T} +_ +T{ \fBfile\fP T} T{ Manage information about files on the minion, set/read user, group, and mode @@ -4808,6 +5100,12 @@ Salt module to manage RAID arrays with mdadm T} _ T{ +\fBmonit\fP +T} T{ +Monit service module. +T} +_ +T{ \fBmoosefs\fP T} T{ Module for gathering and managing information about MooseFS @@ -4838,6 +5136,24 @@ Support for nginx T} _ T{ +\fBnzbget\fP +T} T{ +Support for nzbget +T} +_ +T{ +\fBopenbsdpkg\fP +T} T{ +Package support for OpenBSD +T} +_ +T{ +\fBopenbsdservice\fP +T} T{ +The service module for OpenBSD +T} +_ +T{ \fBosxdesktop\fP T} T{ Mac OS X implementations of various commands in the "desktop" interface @@ -4862,12 +5178,24 @@ Install Python packages with pip to either the system or a virtualenv T} _ T{ +\fBpkgng\fP +T} T{ +Support for pkgng +T} +_ +T{ \fBpostgres\fP T} T{ Module to provide Postgres compatibility to salt. T} _ T{ +\fBpoudriere\fP +T} T{ +Support for poudriere +T} +_ +T{ \fBps\fP T} T{ A salt interface to psutil, a system and process library. @@ -5072,15 +5400,20 @@ Manage Windows users with the net user command T} _ T{ +\fByumpkg5\fP +T} T{ +Support for YUM +T} +_ +T{ \fByumpkg\fP T} T{ Support for YUM T} _ T{ -\fByumpkg5\fP +\fBzfs\fP T} T{ -Support for YUM T} _ T{ @@ -5690,6 +6023,106 @@ salt \(aq*\(aq augeas.tree /files/etc/ .ft P .fi .UNINDENT +.SS salt.modules.bluez +.sp +Support for Bluetooth (using Bluez in Linux) +.INDENT 0.0 +.TP +.B salt.modules.bluez.address() +Get the many addresses of the Bluetooth adapter +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetooth.address +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.bluez.pair(address, key) +Pair the bluetooth adapter with a device +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetooth.pair DE:AD:BE:EF:CA:FE 1234 +.ft P +.fi +.sp +Where DE:AD:BE:EF:CA:FE is the address of the device +to pair with, and 1234 is the passphrase. +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.bluez.scan() +Scan for bluetooth devices in the area +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetooth.scan +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.bluez.start() +Start the bluetooth service. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetooth.start +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.bluez.stop() +Stop the bluetooth service. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetooth.stop +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.bluez.unpair(address) +Unpair the bluetooth adapter from a device +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetooth.unpair DE:AD:BE:EF:CA:FE +.ft P +.fi +.sp +Where DE:AD:BE:EF:CA:FE is the address of the device +to unpair. +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.bluez.version() +Return Bluez version from bluetoothd \-v +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq bluetoothd.version +.ft P +.fi +.UNINDENT .SS salt.modules.brew .INDENT 0.0 .TP @@ -5872,6 +6305,310 @@ salt \(aq*\(aq buttervm.local_images .ft P .fi .UNINDENT +.SS salt.modules.ca +.sp +A salt interface for running a Certificate Authority (CA) +which provides signed/unsigned SSL certificates +.sp +REQUIREMENT 1: +.sp +Required python modules: PyOpenSSL +.sp +REQUIREMENT 2: +.sp +Add the following values in /etc/salt/minion for the +CA module to function properly: +.sp +ca.cert_base_path: \(aq/etc/pki/koji\(aq +.INDENT 0.0 +.TP +.B salt.modules.ca.create_ca(ca_name, bits=2048, days=365, CN=\(aqlocalhost\(aq, C=\(aqUS\(aq, ST=\(aqUtah\(aq, L=\(aqSalt Lake City\(aq, O=\(aqSalt Stack\(aq, OU=None, emailAddress=\(aqxyz@pdq.net\(aq) +Create a Certificate Authority (CA) +.INDENT 7.0 +.TP +.B ca_name +name of the CA +.TP +.B bits +number of RSA key bits, default is 2048 +.TP +.B days +number of days the CA will be valid, default is 365 +.TP +.B CN +common name in the request, default is "localhost" +.TP +.B C +country, default is "US" +.TP +.B ST +state, default is "Utah" +.TP +.B L +locality, default is "Centerville", the city where SaltStack originated +.TP +.B O +organization, default is "Salt Stack" +.TP +.B OU +organizational unit, default is None +.TP +.B email +email address for the CA owner, default is \fI\%'xyz@pdq.net\fP\(aq +.UNINDENT +.sp +Writes out a CA certificate based upon defined config values. If the file +already exists, the function just returns assuming the CA certificate +already exists. +.sp +If the following values were set: +.sp +ca.cert_base_path=\(aq/etc/pki/koji\(aq +ca_name=\(aqkoji\(aq +.sp +the resulting CA would be written in the following location: +.sp +.nf +.ft C +/etc/pki/koji/koji_ca_cert.crt +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.ca.create_ca_signed_cert(ca_name, CN, days=365) +Create a Certificate (CERT) signed by a +particular Certificate Authority (CA) +.INDENT 7.0 +.TP +.B ca_name +name of the CA +.TP +.B CN +common name matching the the certificate signing request +.TP +.B days +number of days certficate is valid, default is 365 (1 year) +.UNINDENT +.sp +Writes out a Certificate (CERT) If the file already +exists, the function just returns assuming the CERT already exists. +.sp +The CN \fImust\fP match an existing CSR generated by create_csr. If it +does not, this method does nothing. +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.ca.create_csr(ca_name, bits=2048, CN=\(aqlocalhost\(aq, C=\(aqUS\(aq, ST=\(aqUtah\(aq, L=\(aqSalt Lake City\(aq, O=\(aqSalt Stack\(aq, OU=None, emailAddress=\(aqxyz@pdq.net\(aq) +Create a Certificate Signing Request (CSR) for a +particular Certificate Authority (CA) +.INDENT 7.0 +.TP +.B ca_name +name of the CA +.TP +.B bits +number of RSA key bits, default is 2048 +.TP +.B CN +common name in the request, default is "localhost" +.TP +.B C +country, default is "US" +.TP +.B ST +state, default is "Utah" +.TP +.B L +locality, default is "Centerville", the city where SaltStack originated +.TP +.B O +organization, default is "Salt Stack" +NOTE: Must the same as CA certificate or an error will be raised +.TP +.B OU +organizational unit, default is None +.TP +.B emailAddress +email address for the request, default is \fI\%'xyz@pdq.net\fP\(aq +.UNINDENT +.sp +Writes out a Certificate Signing Request (CSR) If the file already +exists, the function just returns assuming the CSR already exists. +.sp +If the following values were set: +.sp +ca.cert_base_path=\(aq/etc/pki/koji\(aq +ca_name=\(aqkoji\(aq +CN=\(aqtest.egavas.org\(aq +.sp +the resulting CSR, and corresponding key, would be written in the +following location: +.sp +/etc/pki/koji/certs/test.egavas.org.csr +/etc/pki/koji/certs/test.egavas.org.key +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.ca.create_pkcs12(ca_name, CN, passphrase=\(aq\(aq) +Create a PKCS#12 browser certificate for a particular Certificate (CN) +.INDENT 7.0 +.TP +.B ca_name +name of the CA +.TP +.B CN +common name matching the the certificate signing request +.TP +.B passphrase +used to unlock the PKCS#12 certificate when loaded into the browser +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.ca.create_self_signed_cert(bits=2048) +Create a Self\-Signed Certificate (CERT) \-\- Not yet implemented +.UNINDENT +.SS salt.modules.cassandra +.sp +Cassandra NoSQL Database Module +.sp +REQUIREMENT 1: +.sp +The location of the \(aqnodetool\(aq command, host, and thrift port +needs to be specified via pillar. +.INDENT 0.0 +.INDENT 3.5 +cassandra.nodetool: /usr/local/bin/nodetool +cassandra.host: localhost +cassandra.thrift_port: 9160 +.UNINDENT +.UNINDENT +.sp +REQUIREMENT 2: +.sp +The python module, \(aqpycassa\(aq, also needs to be installed on the +minion. +.INDENT 0.0 +.TP +.B salt.modules.cassandra.column_families(keyspace=None) +Return existing column families for all keyspaces +or just the provided one. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.column_families +salt \(aq*\(aq cassandra.column_families +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.column_family_definition(keyspace=None, column_family=None) +Return a dictionary of column family definitions for the given +keyspace/column_family +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.column_family_definition +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.compactionstats() +Return compactionstats info +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.compactionstats +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.info() +Return cassandra node info +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.info +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.keyspaces() +Return existing keyspaces +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.keyspaces +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.netstats() +Return netstats info +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.netstats +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.ring() +Return cassandra ring info +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.ring +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.tpstats() +Return tpstats info +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.tpstats +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.cassandra.version() +Return the cassandra version +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq cassandra.version +.ft P +.fi +.UNINDENT .SS salt.modules.cluster .sp The cluster module is used to distribute and activate salt HA cluster @@ -5983,7 +6720,7 @@ salt \(aq*\(aq cmd.run_stdout "ls \-l | awk \(aq/foo/{print $2}\(aq" .UNINDENT .INDENT 0.0 .TP -.B salt.modules.cmdmod.script(source, cwd=None, runas=None, shell=\(aq/bin/bash\(aq, env=\(aqbase\(aq, template=\(aqjinja\(aq, **kwargs) +.B salt.modules.cmdmod.script(source, args=None, cwd=None, runas=None, shell=\(aq/bin/bash\(aq, env=\(aqbase\(aq, template=\(aqjinja\(aq, **kwargs) Download a script from a remote location and execute the script locally. The script can be located on the salt master file server or on an http/ftp server. @@ -5992,12 +6729,14 @@ The script will be executed directly, so it can be written in any available programming language. .sp The script can also be formated as a template, the default is jinja. +Arguments for the script can be specified as well. .sp CLI Example: .sp .nf .ft C salt \(aq*\(aq cmd.script salt://scripts/runme.sh +salt \(aq*\(aq cmd.script salt://scripts/runme.sh \(aqarg1 arg2 "arg 3"\(aq .ft P .fi .UNINDENT @@ -6216,6 +6955,19 @@ salt \(aq*\(aq cp.list_master .UNINDENT .INDENT 0.0 .TP +.B salt.modules.cp.list_master_dirs(env=\(aqbase\(aq) +List all of the directories stored on the master +.sp +CLI Exmaple: +.sp +.nf +.ft C +salt \(aq*\(aq cp.list_master_dirs +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.cp.list_minion(env=\(aqbase\(aq) List all of the files cached on the minion .sp @@ -6615,6 +7367,19 @@ salt \(aq*\(aq service.get_enabled .UNINDENT .INDENT 0.0 .TP +.B salt.modules.debian_service.reload(name) +Reload the named service +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq service.reload +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.debian_service.restart(name) Restart the named service .sp @@ -6696,78 +7461,6 @@ salt \(aq*\(aq disk.usage .ft P .fi .UNINDENT -.SS salt.modules.django -.sp -Manage Django sites -.INDENT 0.0 -.TP -.B salt.modules.django.collectstatic(settings_module, bin_env=None, no_post_process=False, ignore=None, dry_run=False, clear=False, link=False, no_default_ignore=False, pythonpath=None) -Collect static files from each of your applications into a single location -that can easily be served in production. -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq django.collectstatic settings.py -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.django.command(settings_module, command, bin_env=None, pythonpath=None, *args, **kwargs) -Run arbitrary django management command -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.django.createsuperuser(settings_module, username, email, bin_env=None, database=None, pythonpath=None) -Create a super user for the database. -This function defaults to use the \fB\-\-noinput\fP flag which prevents the -creation of a password for the superuser. -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq django.createsuperuser settings.py user user@example.com -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.django.loaddata(settings_module, fixtures, bin_env=None, database=None, pythonpath=None) -Load fixture data -.INDENT 7.0 -.TP -.B Fixtures: -comma separated list of fixtures to load -.UNINDENT -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq django.loaddata settings.py -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.django.syncdb(settings_module, bin_env=None, migrate=False, database=None, pythonpath=None, noinput=True) -Run syncdb -.sp -Execute the Django\-Admin syncdb command, if South is available on the -minion the \fBmigrate\fP option can be passed as \fBTrue\fP calling the -migrations to run after the syncdb completes -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq django.syncdb settings.py -.ft P -.fi -.UNINDENT .SS salt.modules.ebuild .sp Support for Portage @@ -6925,6 +7618,35 @@ salt \(aq*\(aq pkg.version .ft P .fi .UNINDENT +.SS salt.modules.event +.sp +Fire events on the minion, events can be fired up to the master +.INDENT 0.0 +.TP +.B salt.modules.event.fire(data, tag) +Fire an event on the local minion event bus +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq event.fire \(aqstuff to be in the event\(aq \(aqtag\(aq +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.event.fire_master(data, tag) +Fire an event off on the master server +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq event.fire_master \(aqstuff to be in the event\(aq \(aqtag\(aq +.ft P +.fi +.UNINDENT .SS salt.modules.file .sp Manage information about files on the minion, set/read user, group, and mode @@ -7705,7 +8427,7 @@ salt \(aq*\(aq pkg.available_version .UNINDENT .INDENT 0.0 .TP -.B salt.modules.freebsdpkg.install(name, *args, **kwargs) +.B salt.modules.freebsdpkg.install(name, refresh=False, repo=\(aq\(aq, **kwargs) Install the passed package .sp Return a dict containing the new package names and versions: @@ -7762,8 +8484,8 @@ salt \(aq*\(aq pkg.purge .INDENT 0.0 .TP .B salt.modules.freebsdpkg.refresh_db() -Use pkg update to get latest repo.txz when using pkgng, else update the -ports tree with portsnap otherwise. If the ports tree does not exist it +Use pkg update to get latest repo.txz when using pkgng, else update the +ports tree with portsnap otherwise. If the ports tree does not exist it will be downloaded and set up. .sp CLI Example: @@ -7813,14 +8535,14 @@ CLI Example: .sp .nf .ft C -salt \(aq*\(aq pkg.pkgng_search \(aqmysql\-server\(aq +salt \(aq*\(aq pkg.search \(aqmysql\-server\(aq .ft P .fi .UNINDENT .INDENT 0.0 .TP .B salt.modules.freebsdpkg.upgrade() -Run a full system upgrade, a \fBfreebsd\-update fetch install\fP +Run pkg upgrade, if pkgng used. Otherwise do nothing .sp Return a dict containing the new package names and versions: .sp @@ -9445,6 +10167,42 @@ salt \(aq*\(aq raid.list .ft P .fi .UNINDENT +.SS salt.modules.monit +.sp +Monit service module. This module will create a monit type +service watcher. +.INDENT 0.0 +.TP +.B salt.modules.monit.restart(name) +Restart service via monit +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq monit.restart +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.monit.start(name) +CLI Example:: +salt \(aq*\(aq monit.start +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.monit.stop(name) +Stops service via monit +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq monit.stop +.ft P +.fi +.UNINDENT .SS salt.modules.moosefs .sp Module for gathering and managing information about MooseFS @@ -9597,6 +10355,19 @@ salt \(aq*\(aq mount.set_fstab /mnt/foo /dev/sdz1 ext4 .ft P .fi .UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.mount.umount(name) +Attempt to unmount a device by specifying the directory it is mounted on +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq mount.umount /mnt/foo +.ft P +.fi +.UNINDENT .SS salt.modules.mysql .sp Module to provide MySQL compatibility to salt. @@ -9754,7 +10525,7 @@ CLI Example: .sp .nf .ft C -salt \(aq*\(aq mysql.grant_add \(aqSELECT|INSERT|UPDATE|...\(aq \(aqdatabase.*\(aq \(aqfrank\(aq \(aqlocalhost\(aq +salt \(aq*\(aq mysql.grant_add \(aqSELECT,INSERT,UPDATE,...\(aq \(aqdatabase.*\(aq \(aqfrank\(aq \(aqlocalhost\(aq .ft P .fi .UNINDENT @@ -9973,7 +10744,21 @@ salt \(aq*\(aq network.dig archlinux.org .UNINDENT .INDENT 0.0 .TP +.B salt.modules.network.in_subnet(cidr) +Returns True if host is within specified subnet, otherwise False +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.network.interfaces() +Return a dictionary of information about all the interfaces on the minion +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq network.interfaces +.ft P +.fi .UNINDENT .INDENT 0.0 .TP @@ -10003,6 +10788,11 @@ salt \(aq*\(aq network.ping archlinux.org .UNINDENT .INDENT 0.0 .TP +.B salt.modules.network.subnets() +Returns a list of subnets to which the host belongs +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.network.traceroute(host) Performs a traceroute to a 3rd party host .sp @@ -10020,7 +10810,7 @@ Support for nginx .INDENT 0.0 .TP .B salt.modules.nginx.signal(signal=None) -Signals httpd to start, restart, or stop. +Signals nginx to start, restart, or stop. .sp CLI Example: .sp @@ -10043,6 +10833,262 @@ salt \(aq*\(aq nginx.version .ft P .fi .UNINDENT +.SS salt.modules.nzbget +.sp +Support for nzbget +.INDENT 0.0 +.TP +.B salt.modules.nzbget.list(user=None) +Return list of active downloads using nzbget \-L. +Default user is root. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.list larry +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.nzbget.pause(user=None) +Pause nzbget daemon using \-P option. +Default user is root. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.pause shemp +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.nzbget.serverversion() +Return server version from nzbget \-V. +Default user is root. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.serverversion moe +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.nzbget.start(user=None) +Start nzbget as a daemon using \-D option +Default user is root. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.start +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.nzbget.stop(user=None) +Stop nzbget daemon using \-Q option. +Default user is root. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.stop curly +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.nzbget.unpause(user=None) +Unpause nzbget daemon using \-U option. +Default user is root. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.unpause shemp +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.nzbget.version() +Return version from nzbget \-v. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq nzbget.version +.ft P +.fi +.UNINDENT +.SS salt.modules.openbsdpkg +.sp +Package support for OpenBSD +.INDENT 0.0 +.TP +.B salt.modules.openbsdpkg.available_version(name) +The available version of the package in the repository +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.available_version +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdpkg.install(name, *args, **kwargs) +Install the passed package +.sp +Return a dict containing the new package names and versions: +.sp +.nf +.ft C +{\(aq\(aq: {\(aqold\(aq: \(aq\(aq, + \(aqnew\(aq: \(aq\(aq]} +.ft P +.fi +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.install +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdpkg.list_pkgs() +List the packages currently installed as a dict: +.sp +.nf +.ft C +{\(aq\(aq: \(aq\(aq} +.ft P +.fi +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.list_pkgs +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdpkg.purge(name) +Remove a single package with pkg_delete +.sp +Returns a list containing the removed packages. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.purge +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdpkg.remove(name) +Remove a single package with pkg_delete +.sp +Returns a list containing the removed packages. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.remove +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdpkg.version(name) +Returns a version if the package is installed, else returns an empty string +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.version +.ft P +.fi +.UNINDENT +.SS salt.modules.openbsdservice +.sp +The service module for OpenBSD +.INDENT 0.0 +.TP +.B salt.modules.openbsdservice.restart(name) +Restart the named service +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq service.restart +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdservice.start(name) +Start the specified service +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq service.start +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdservice.status(name, sig=None) +Return the status for a service, returns a bool whether the service is +running. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq service.status +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.openbsdservice.stop(name) +Stop the specified service +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq service.stop +.ft P +.fi +.UNINDENT .SS salt.modules.osxdesktop .sp Mac OS X implementations of various commands in the "desktop" interface @@ -10339,7 +11385,7 @@ If installing into a virtualenv, just use the path to the virtualenv (/home/code/path/to/virtualenv/) .TP .B env -depreicated, use bin_env now +deprecated, use bin_env now .TP .B log Log file where a complete (maximum verbosity) record will be kept @@ -10448,8 +11494,8 @@ salt \(aq*\(aq pip.install markdown,django editable=git+https://github.com/world .INDENT 0.0 .TP .B salt.modules.pip.list(prefix=\(aq\(aq, bin_env=None, runas=None, cwd=None) -Filter list of instaslled apps from \fBfreeze\fP and check to see if \fBprefix\fP -exists in the list of packages installed. +Filter list of installed apps from \fBfreeze\fP and check to see if +\fBprefix\fP exists in the list of packages installed. .sp CLI Example: .sp @@ -10516,6 +11562,89 @@ salt \(aq*\(aq pip.uninstall bin_env=/path/to/pip_bin .ft P .fi .UNINDENT +.SS salt.modules.pkgng +.sp +Support for pkgng +.INDENT 0.0 +.TP +.B salt.modules.pkgng.add(pkg_path) +Adds files from remote or local package +.INDENT 7.0 +.TP +.B CLI Example:: +salt \(aq*\(aq pkgng.add /tmp/package.txz +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.backup(file_name) +Export installed packages into yaml+mtree file +.INDENT 7.0 +.TP +.B CLI Example:: +salt \(aq*\(aq pkgng.backup /tmp/pkg +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.info(pkg=None) +Returns info on packages installed on system +.INDENT 7.0 +.TP +.B CLI Example:: +salt \(aq*\(aq pkgng.info +.sp +For individual info +.sp +salt \(aq*\(aq pkgng.info sudo +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.parse_config(file_name=\(aq/usr/local/etc/pkg.conf\(aq) +Return dict of uncommented global variables. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkgng.parse_config +*NOTE* not working right +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.restore(file_name) +Reads archive created by pkg backup \-d and recreates the database. +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.stats() +Return pkgng stats. +.INDENT 7.0 +.TP +.B CLI Example:: +salt \(aq*\(aq pkgng.stats +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.update_package_site(new_url) +Updates remote package repo url, PACKAGESITE var to be exact. +.sp +Must be using \fI\%http://\fP, \fI\%ftp://\fP, or https// protos +.INDENT 7.0 +.TP +.B CLI Example:: +salt \(aq*\(aq pkgng.update_package_site \fI\%http://127.0.0.1/\fP +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.pkgng.version() +return the version of pkgng +.UNINDENT .SS salt.modules.postgres .sp Module to provide Postgres compatibility to salt. @@ -10670,6 +11799,132 @@ salt \(aq*\(aq postgres.version .ft P .fi .UNINDENT +.SS salt.modules.poudriere +.sp +Support for poudriere +.INDENT 0.0 +.TP +.B salt.modules.poudriere.bulk_build(jail, pkg_file, keep=False) +Run bulk build on poudriere server. +.sp +Return number of pkg builds, failures, and errors, on error dump to cli +.sp +CLI Example: +.sp +.nf +.ft C +salt \-N buildbox_group poudriere.bulk_build 90amd64 /root/pkg_list +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.create_jail(name, arch, version=\(aq9.0\-RELEASE\(aq) +Creates a new poudriere jail if one does not exist +.sp +\fINOTE\fP creating a new jail will take some time the master is not hanging +.INDENT 7.0 +.TP +.B CLI Example:: +salt \(aq*\(aq poudriere.create_jail 90amd64 amd64 +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.create_ports_tree() +Not working need to run portfetch non interactive +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.delete_jail(name) +Deletes poudriere jail with \fIname\fP +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.delete_jail 90amd64 +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.is_jail(name) +Return True if jail exists False if not +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.is_jail +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.list_jails() +Return a list of current jails managed by poudriere +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.list_jails +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.list_ports() +Return a list of current port trees managed by poudriere +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.list_ports +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.make_pkgng_aware(jname) +Make jail \fBjname\fP pkgng aware +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.make_pkgng_aware +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.parse_config(config_file=None) +Returns a dict of poudriere main configuration defintions +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.parse_config +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.poudriere.version() +Return poudriere version +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq poudriere.version +.ft P +.fi +.UNINDENT .SS salt.modules.ps .sp A salt interface to psutil, a system and process library. @@ -10933,6 +12188,10 @@ salt system.example.com publish.full_data \(aq*\(aq cmd.run \(aqls \-la /tmp\(aq .UNINDENT .INDENT 0.0 .TP +.B salt.modules.publish.normalize_arg(arg) +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.publish.publish(tgt, fun, arg=None, expr_form=\(aqglob\(aq, returner=\(aq\(aq, timeout=5) Publish a command from the minion out to other minions, publications need to be enabled on the Salt master and the minion needs to have permission @@ -11196,7 +12455,7 @@ CLI Example: .sp .nf .ft C -salt \(aq*\(aq user.delete name True True +salt \(aq*\(aq user.delete name remove=True force=True .ft P .fi .UNINDENT @@ -11932,7 +13191,7 @@ salt \(aq*\(aq saltutil.find_job .INDENT 0.0 .TP .B salt.modules.saltutil.kill_job(jid) -Sends a termination signal (SIGTERM 15) to the named salt job\(aqs process +Sends a kill signal (SIGKILL 9) to the named salt job\(aqs process .sp CLI Example: .sp @@ -11983,7 +13242,7 @@ salt \(aq*\(aq saltutil.signal_job 15 .UNINDENT .INDENT 0.0 .TP -.B salt.modules.saltutil.sync_all(env=\(aqbase\(aq) +.B salt.modules.saltutil.sync_all(env=None) Sync down all of the dynamic modules from the file server for a specific environment .sp @@ -11997,7 +13256,7 @@ salt \(aq*\(aq saltutil.sync_all .UNINDENT .INDENT 0.0 .TP -.B salt.modules.saltutil.sync_grains(env=\(aqbase\(aq) +.B salt.modules.saltutil.sync_grains(env=None) Sync the grains from the _grains directory on the salt master file server. This function is environment aware, pass the desired environment to grab the contents of the _grains directory, base is the default @@ -12013,7 +13272,7 @@ salt \(aq*\(aq saltutil.sync_grains .UNINDENT .INDENT 0.0 .TP -.B salt.modules.saltutil.sync_modules(env=\(aqbase\(aq) +.B salt.modules.saltutil.sync_modules(env=None) Sync the modules from the _modules directory on the salt master file server. This function is environment aware, pass the desired environment to grab the contents of the _modules directory, base is the default @@ -12029,7 +13288,7 @@ salt \(aq*\(aq saltutil.sync_modules .UNINDENT .INDENT 0.0 .TP -.B salt.modules.saltutil.sync_renderers(env=\(aqbase\(aq) +.B salt.modules.saltutil.sync_renderers(env=None) Sync the renderers from the _renderers directory on the salt master file server. This function is environment aware, pass the desired environment to grab the contents of the _renderers directory, base is the default @@ -12045,7 +13304,7 @@ salt \(aq*\(aq saltutil.sync_renderers .UNINDENT .INDENT 0.0 .TP -.B salt.modules.saltutil.sync_returners(env=\(aqbase\(aq) +.B salt.modules.saltutil.sync_returners(env=None) Sync the returners from the _returners directory on the salt master file server. This function is environment aware, pass the desired environment to grab the contents of the _returners directory, base is the default @@ -12061,7 +13320,7 @@ salt \(aq*\(aq saltutil.sync_returners .UNINDENT .INDENT 0.0 .TP -.B salt.modules.saltutil.sync_states(env=\(aqbase\(aq) +.B salt.modules.saltutil.sync_states(env=None) Sync the states from the _states directory on the salt master file server. This function is environment aware, pass the desired environment to grab the contents of the _states directory, base is the default @@ -12088,6 +13347,26 @@ salt \(aq*\(aq saltutil.term_job .ft P .fi .UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.saltutil.update(version=None) +Update the salt minion from the url defined in opts[\(aqupdate_url\(aq] +.sp +This feature requires the minion to be running a bdist_esky build. +.sp +The version number is optional and will default to the most recent version +available at opts[\(aqupdate_url\(aq]. +.sp +Returns details about the transaction upon completion. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq saltutil.update 0.10.3 +.ft P +.fi +.UNINDENT .SS salt.modules.selinux .sp Execute calls on selinux @@ -12272,6 +13551,19 @@ salt \(aq*\(aq shadow.info root .UNINDENT .INDENT 0.0 .TP +.B salt.modules.shadow.set_date(name, date) +sets the value for the date the password was last changed to the epoch (January 1, 1970). See man chage. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq shadow.set_date username 0 +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.shadow.set_inactdays(name, inactdays) Set the number of days of inactivity after a password has expired before the account is locked. See man chage. .sp @@ -13291,13 +14583,16 @@ salt \(aq*\(aq ssh.rm_known_host .INDENT 0.0 .TP .B salt.modules.ssh.set_auth_key(user, key, enc=\(aqssh\-rsa\(aq, comment=\(aq\(aq, options=[], config=\(aq.ssh/authorized_keys\(aq) -Add a key to the authorized_keys file +Add a key to the authorized_keys file. The "key" parameter must only be the +string of text that is the encoded key. If the key begins with "ssh\-rsa" +or ends with \fI\%user@host\fP, remove those from the key before passing it to this +function. .sp CLI Example: .sp .nf .ft C -salt \(aq*\(aq ssh.set_auth_key key=\(aq\(aq enc=\(aqdsa\(aq comment=\(aqmy key\(aq options=\(aq[]\(aq config=\(aq.ssh/authorized_keys\(aq +salt \(aq*\(aq ssh.set_auth_key \(aq\(aq enc=\(aqdsa\(aq .ft P .fi .UNINDENT @@ -13424,13 +14719,13 @@ CLI Example: .sp .nf .ft C -salt \(aq*\(aq state.sls core,edit.vim dev +salt \(aq*\(aq state.show_sls core,edit.vim dev .ft P .fi .UNINDENT .INDENT 0.0 .TP -.B salt.modules.state.single(fun=None, test=None, **kwargs) +.B salt.modules.state.single(fun, name, test=None, **kwargs) Execute a single state function with the named kwargs, returns False if insufficient data is sent to the command .sp @@ -13660,6 +14955,20 @@ salt \(aq*\(aq status.netstats .UNINDENT .INDENT 0.0 .TP +.B salt.modules.status.pid(sig) +Return the PID or an empty string if the process is running or not. +Pass a signature to use to find the process via ps. +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq status.pid +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.status.procs() Return the process data .sp @@ -14088,7 +15397,7 @@ start on ((((filesystem and runlevel [!06]) and started dbus) and (drm\-device\- stop on runlevel [016] .UNINDENT .sp -DO NOT use this module on red hat systems, as red hat systems should use the +DO NOT use this module on Red Hat systems, as Red Hat systems should use the rh_service module, since red hat systems support chkconfig .INDENT 0.0 .TP @@ -14144,6 +15453,19 @@ salt \(aq*\(aq service.enabled .UNINDENT .INDENT 0.0 .TP +.B salt.modules.upstart.full_restart(name) +Do a full restart (stop/start) of the named service +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq service.full_restart +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.upstart.get_all() Return all installed services .sp @@ -14404,7 +15726,7 @@ CLI Example: .sp .nf .ft C -salt \(aq*\(aq user.delete name True True +salt \(aq*\(aq user.delete name remove=True force=True .ft P .fi .UNINDENT @@ -14493,6 +15815,19 @@ salt \(aq*\(aq virt.create_xml_str .UNINDENT .INDENT 0.0 .TP +.B salt.modules.virt.ctrl_alt_del(vm_) +Sends CTRL+ALT+DEL to a VM +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq virt.ctrl_alt_del +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.virt.destroy(vm_) Hard power down the virtual machine, this is equivalent to pulling the power @@ -14771,6 +16106,32 @@ salt \(aq*\(aq virt.purge .UNINDENT .INDENT 0.0 .TP +.B salt.modules.virt.reboot(vm_) +Reboot a domain via ACPI request +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq virt.reboot +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.virt.reset(vm_) +Reset a VM by emulating the reset button on a physical machine +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq virt.reset +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.virt.resume(vm_) Resume the named vm .sp @@ -14813,6 +16174,40 @@ salt "*" virt.set_autostart .UNINDENT .INDENT 0.0 .TP +.B salt.modules.virt.setmem(vm_, memory, config=False) +Changes the amount of memory allocated to VM. The VM must be shutdown +for this to work. +.sp +memory is to be specified in MB +If config is True then we ask libvirt to modify the config as well +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq virt.setmem myvm 768 +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.virt.setvcpus(vm_, vcpus, config=False) +Changes the amount of vcpus allocated to VM. The VM must be shutdown +for this to work. +.sp +vcpus is an int representing the number to be assigned +If config is True then we ask libvirt to modify the config as well +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq virt.setvcpus myvm 2 +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP .B salt.modules.virt.shutdown(vm_) Send a soft shutdown signal to the named vm .sp @@ -15095,7 +16490,7 @@ New in version 0.9.5. .TP .B salt.modules.win_file.find(path, **kwargs) Approximate the Unix find(1) command and return a list of paths that -meet the specified critera. +meet the specified criteria. .sp The options include match criteria: .sp @@ -16105,6 +17500,186 @@ salt \(aq*\(aq user.setpassword name password .ft P .fi .UNINDENT +.SS salt.modules.yumpkg5 +.sp +Support for YUM +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.available_version(name) +The available version of the package in the repository +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.available_version +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.install(pkg, refresh=False, repo=\(aq\(aq, skip_verify=False, **kwargs) +Install the passed package +.INDENT 7.0 +.TP +.B pkg +The name of the package to be installed +.TP +.B refresh +False +Clean out the yum database before executing +.TP +.B repo +(default) +Specify a package repository to install from +(e.g., \fByum \-\-enablerepo=somerepo\fP) +.TP +.B skip_verify +False +Skip the GPG verification check (e.g., \fB\-\-nogpgcheck\fP) +.UNINDENT +.sp +Return a dict containing the new package names and versions: +.sp +.nf +.ft C +{\(aq\(aq: {\(aqold\(aq: \(aq\(aq, + \(aqnew\(aq: \(aq\(aq]} +.ft P +.fi +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.install +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.list_pkgs() +List the packages currently installed in a dict: +.sp +.nf +.ft C +{\(aq\(aq: \(aq\(aq} +.ft P +.fi +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.list_pkgs +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.list_upgrades() +Check whether or not an upgrade is available for all packages +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.list_upgrades +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.purge(pkg) +Yum does not have a purge, this function calls remove +.sp +Return a list containing the removed packages: +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.purge +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.refresh_db() +Since yum refreshes the database automatically, this runs a yum clean, +so that the next yum operation will have a clean database +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.refresh_db +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.remove(pkg) +Remove a single package with yum remove +.sp +Return a list containing the removed packages: +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.remove +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.upgrade() +Run a full system upgrade, a yum upgrade +.sp +Return a dict containing the new package names and versions: +.sp +.nf +.ft C +{\(aq\(aq: {\(aqold\(aq: \(aq\(aq, + \(aqnew\(aq: \(aq\(aq]} +.ft P +.fi +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.upgrade +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.upgrade_available(name) +Check whether or not an upgrade is available for a given package +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.upgrade_available +.ft P +.fi +.UNINDENT +.INDENT 0.0 +.TP +.B salt.modules.yumpkg5.version(name) +Returns a version if the package is installed, else returns an empty string +.sp +CLI Example: +.sp +.nf +.ft C +salt \(aq*\(aq pkg.version +.ft P +.fi +.UNINDENT .SS salt.modules.yumpkg .sp New in version 0.9.4: This module replaces the "yum" module in previous releases. It is backward @@ -16304,186 +17879,6 @@ salt \(aq*\(aq pkg.version .ft P .fi .UNINDENT -.SS salt.modules.yumpkg5 -.sp -Support for YUM -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.available_version(name) -The available version of the package in the repository -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.available_version -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.install(pkg, refresh=False, repo=\(aq\(aq, skip_verify=False, **kwargs) -Install the passed package -.INDENT 7.0 -.TP -.B pkg -The name of the package to be installed -.TP -.B refresh -False -Clean out the yum database before executing -.TP -.B repo -(default) -Specify a package repository to install from -(e.g., \fByum \-\-enablerepo=somerepo\fP) -.TP -.B skip_verify -False -Skip the GPG verification check (e.g., \fB\-\-nogpgcheck\fP) -.UNINDENT -.sp -Return a dict containing the new package names and versions: -.sp -.nf -.ft C -{\(aq\(aq: {\(aqold\(aq: \(aq\(aq, - \(aqnew\(aq: \(aq\(aq]} -.ft P -.fi -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.install -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.list_pkgs() -List the packages currently installed in a dict: -.sp -.nf -.ft C -{\(aq\(aq: \(aq\(aq} -.ft P -.fi -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.list_pkgs -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.list_upgrades() -Check whether or not an upgrade is available for all packages -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.list_upgrades -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.purge(pkg) -Yum does not have a purge, this function calls remove -.sp -Return a list containing the removed packages: -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.purge -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.refresh_db() -Since yum refreshes the database automatically, this runs a yum clean, -so that the next yum operation will have a clean database -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.refresh_db -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.remove(pkg) -Remove a single package with yum remove -.sp -Return a list containing the removed packages: -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.remove -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.upgrade() -Run a full system upgrade, a yum upgrade -.sp -Return a dict containing the new package names and versions: -.sp -.nf -.ft C -{\(aq\(aq: {\(aqold\(aq: \(aq\(aq, - \(aqnew\(aq: \(aq\(aq]} -.ft P -.fi -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.upgrade -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.upgrade_available(name) -Check whether or not an upgrade is available for a given package -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.upgrade_available -.ft P -.fi -.UNINDENT -.INDENT 0.0 -.TP -.B salt.modules.yumpkg5.version(name) -Returns a version if the package is installed, else returns an empty string -.sp -CLI Example: -.sp -.nf -.ft C -salt \(aq*\(aq pkg.version -.ft P -.fi -.UNINDENT .SS salt.modules.zypper .sp Package support for openSUSE via the zypper package manager @@ -16704,7 +18099,7 @@ serializes the data as json and sets it in redis. .SS Examples .sp The collection of built\-in Salt returners can be found here: -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/returners\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/returners\fP .SH FULL LIST OF BUILTIN RETURNERS .TS center; @@ -16773,6 +18168,20 @@ Return data to a Cassandra ColumnFamily Return data to a mongodb server .sp Required python modules: pymongo +.sp +This returner will send data from the minions to a MongoDB server. To +configure the settings for your MongoDB server, add the following lines +to the minion config files: +.sp +.nf +.ft C +mongo.db: +mongo.host: +mongo.user: +mongo.password: +mongo.port: 27017 +.ft P +.fi .INDENT 0.0 .TP .B salt.returners.mongo_return.returner(ret) @@ -16793,7 +18202,7 @@ Return data to a redis data store .SH STATE FILE BACKUPS .sp In 0.10.2 a new feature was added for backing up files that are replaced by -the file.managed and file.recuse states. The new feature is called the backup +the file.managed and file.recurse states. The new feature is called the backup mode. Setting the backup mode is easy, but is can be set in a number of places. .sp @@ -17326,6 +18735,30 @@ components. \- .ft P .fi +.SH INCLUDE AND EXCLUDE +.sp +Salt sls files can include other sls files and exclude sls files that have been +otherwise included. This allows for an sls file to easily extend or manipulate +other sls files. +.SS Exclude +.sp +The exclude statement, added in Salt 0.10.3 allows an sls to hard exclude +another sls file or a specific id. The component is excluded after the +high data has been compiled, so nothing should be able to override an +exclude. +.sp +Since the exclude can remove an id or an sls the type of component to +exclude needs to be defined. an exclude statement that verifies that the +running highstate does not contain the \fIhttp\fP sls and the \fI/etc/vimrc\fP id +would look like this: +.sp +.nf +.ft C +exclude: + \- sls: http + \- id: /etc/vimrc +.ft P +.fi .SH STATE ENFORCEMENT .sp Salt offers an optional interface to manage the configuration or "state" of the @@ -17493,7 +18926,7 @@ files. The available renderers can be found in the renderers directory in the Salt source code: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/renderers\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/renderers\fP .sp By default SLS files are rendered using jinja as a templating engine, and yaml as the serialization format. Since the rendering system can be extended simply @@ -17711,6 +19144,23 @@ vim: \- order: last .ft P .fi +.sp +Remember that requisite statements override the order option. So the order +option should be applied to the highest component of the requisite chain: +.sp +.nf +.ft C +vim: + pkg.installed: + \- order: last + \- require: + \- file: /etc/vimrc + +/etc/vimrc: + file.managed: + \- source: salt://edit/vimrc +.ft P +.fi .SH STATE PROVIDERS .sp New in version 0.9.8. @@ -17830,7 +19280,181 @@ watches other states, then when the other states make changes on the system the service is reloaded or restarted. .SS Use .sp -# This needs to be filled in +The \fBuse\fP requisite is used to inherit the arguments passed in another +id declaration. This is useful when many files need to have the same defaults. +.sp +The \fBuse\fP statement was developed primarily for the networking states but +can be used on any states in Salt. This made sense for the networking state +because it can define a long list of options that need to be applied to +multiple network interfaces. +.SS Require In +.sp +The \fBrequire_in\fP requisite is the literal reverse of \fBrequire\fP. If +a state declaration needs to be required by another state declaration then +require_in can accommodate it, so these two sls files would be the same in +the end: +.sp +Using \fBrequire\fP +.sp +.nf +.ft C +httpd: + pkg: + \- installed + service: + \- running + \- require: + \- pkg: httpd +.ft P +.fi +.sp +Using \fBrequire_in\fP +.sp +.nf +.ft C +httpd: + pkg: + \- installed + \- require_in: + \- service: httpd + service: + \- running +.ft P +.fi +.sp +The \fBrequire_in\fP statement is particularly useful when assigning a require +in a sperate sls file. For instance it may be common for httpd to require +components used to set up php or mod_python, but the http state does not need +to be aware of the additional components that require it when it is set up: +.sp +http.sls +.sp +.nf +.ft C +httpd: + pkg: + \- installed + service: + \- running + \- require: + \- pkg: httpd +.ft P +.fi +.sp +php.sls +.sp +.nf +.ft C +include: + \- http + +php: + pkg: + \- installed + \- require_in: + \- service: httpd +.ft P +.fi +.sp +mod_python.sls +.sp +.nf +.ft C +include: + \- http + +mod_python: + pkg: + \- installed + \- require_in: + \- service: httpd +.ft P +.fi +.sp +Now the httpd server will only start if php or mod_python are first verified to +be installed. Thus allowing for a requisite to be defined "after the fact". +.SS Watch In +.sp +Watch in functions the same was as require in, but applies a watch statement +rather than a require statement to the external state declaration. +.SH STARTUP STATES +.sp +Sometimes it may be desired that the salt minion execute a state run when it is +started. This alleviates the need for the master to initiate a state run on a +new minion and can make provisioning much easier. +.sp +As of Salt 0.10.3 the minion config reads options that allow for states to be +executed at startup. The options are \fIstartup_states\fP, \fIsls_list\fP and +\fItop_file\fP. +.sp +The \fIstartup_states\fP option can be passed one of a number of arguments to +define how to execute states. The available options are: +.INDENT 0.0 +.TP +.B highstate +Execute \fBstate.highstate\fP +.TP +.B sls +Read in the \fBsls_list\fP option and execute the named sls files +.TP +.B top +Read in the \fBtop_file\fP option and execute states based on that top file +on the Salt Master +.UNINDENT +.SS Examples: +.sp +Execute \fBstate.highstate\fP when starting the minion: +.sp +.nf +.ft C +startup_states: highstate +.ft P +.fi +.sp +Execute the sls files \fIedit.vim\fP and \fIhyper\fP: +.sp +.nf +.ft C +startup_states: sls + +sls_list: + \- edit.vim + \- hyper +.ft P +.fi +.SH STATE TESTING +.sp +Executing a Salt state run can potentially change many aspects of a system and +it may be desirable to first see what a state run is going to change before +applying the run. +.sp +Salt has a test interface to report on exactly what will be changed, this +interface can be invoked on any of the major state run functions: +.sp +.nf +.ft C +# salt \e* state.highstate test=True +# salt \e* state.sls test=True +# salt \e* state.single test=True +.ft P +.fi +.sp +The test run is mandated by adding the \fBtest=True\fP option to the states. The +return information will show states that will be applied in yellow and the +result is reported as \fINone\fP. +.SS Default Test +.sp +If the value \fItest\fP is set to True in the minion configuration file then states +will default to being executed in test mode. If this value is set then states +can still be run by calling test=False: +.sp +.nf +.ft C +# salt \e* state.highstate test=False +# salt \e* state.sls test=False +# salt \e* state.single test=False +.ft P +.fi .SH THE TOP FILE .sp The top file is used to map what SLS modules get loaded onto what minions via @@ -18252,6 +19876,12 @@ Loading and unloading of kernel modules. T} _ T{ +\fBmodule\fP +T} T{ +Execution of Salt modules from within states. +T} +_ +T{ \fBmount\fP T} T{ Mounting of filesystems. @@ -18288,6 +19918,12 @@ Installation of Python packages using pip. T} _ T{ +\fBpkgng\fP +T} T{ +Manage package remote repo using FreeBSD pkgng. +T} +_ +T{ \fBpkg\fP T} T{ Installation of packages using OS package managers such as yum or apt\-get. @@ -18300,6 +19936,12 @@ Management of PostgreSQL databases (schemas). T} _ T{ +\fBpostgres_user\fP +T} T{ +Management of PostgreSQL users (roles). +T} +_ +T{ \fBrvm\fP T} T{ Managing Ruby installations and gemsets with Ruby Version Manager (RVM). @@ -18733,7 +20375,7 @@ takes a few arguments: Recursive directory management can also be set via the \fBrecurse\fP function. Recursive directory management allows for a directory on the salt master to be recursively copied down to the minion. This is a great tool for -deploying large code and configuration systems. A recuse state would look +deploying large code and configuration systems. A recurse state would look something like this: .sp .nf @@ -18756,7 +20398,7 @@ The path which should be deleted .UNINDENT .INDENT 0.0 .TP -.B salt.states.file.append(name, text) +.B salt.states.file.append(name, text=None, makedirs=False, source=None, source_hash=None) Ensure that some text appears at the end of a file .sp The text will not be appended again if it already exists in the file. You @@ -18792,6 +20434,33 @@ New in version 0.9.5. .INDENT 0.0 .TP .B salt.states.file.comment(name, regex, char=\(aq#\(aq, backup=\(aq.bak\(aq) +Comment out specified lines in a file. +.INDENT 7.0 +.TP +.B path +The full path to the file to be edited +.TP +.B regex +A regular expression used to find the lines that are to be commented; +this pattern will be wrapped in parenthesis and will move any +preceding/trailing \fB^\fP or \fB$\fP characters outside the parenthesis +(e.g., the pattern \fB^foo$\fP will be rewritten as \fB^(foo)$\fP) +.TP +.B char +\fB#\fP +The character to be inserted at the beginning of a line in order to +comment it out +.TP +.B backup +\fB.bak\fP +The file will be backed up before edit with this file extension +.IP Warning +This backup will be overwritten each time \fBsed\fP / \fBcomment\fP / +\fBuncomment\fP is called. Meaning the backup will only be useful +after the first invocation. +.RE +.UNINDENT +.sp Usage: .sp .nf @@ -18844,6 +20513,18 @@ Require other resources such as packages or files .UNINDENT .INDENT 0.0 .TP +.B salt.states.file.exists(name) +Verify that the named file or directory is present or exists. +Ensures pre\-requisites outside of salts per\-vue have been previously +satisified (aka, keytabs, private keys, etc.) before deployment +.INDENT 7.0 +.TP +.B name +Absolute path which must exist +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP .B salt.states.file.managed(name, source=None, source_hash=\(aq\(aq, user=None, group=None, mode=None, template=None, makedirs=False, context=None, replace=True, defaults=None, env=None, backup=\(aq\(aq, **kwargs) Manage a given file, this function allows for a file to be downloaded from the salt master and potentially run through a templating system. @@ -18892,8 +20573,9 @@ directories will be created to facilitate the creation of the named file. .TP .B replace -If this file should be replaced, if false then this command will -be ignored if the file exists already. Default is true. +If this file should be replaced. If false, this command will +not overwrite file contents but will enforce permissions if the file +exists already. Default is true. .TP .B context Overrides default context variables passed to the template. @@ -19046,6 +20728,30 @@ New in version 0.9.5. .INDENT 0.0 .TP .B salt.states.file.uncomment(name, regex, char=\(aq#\(aq, backup=\(aq.bak\(aq) +Uncomment specified commented lines in a file +.INDENT 7.0 +.TP +.B path +The full path to the file to be edited +.TP +.B regex +A regular expression used to find the lines that are to be uncommented. +This regex should not include the comment character. A leading \fB^\fP +character will be stripped for convenience (for easily switching +between comment() and uncomment()). +.TP +.B char +\fB#\fP +The character to remove in order to uncomment a line; if a single +whitespace character follows the comment it will also be removed +.TP +.B backup +\fB.bak\fP +The file will be backed up before edit with this file extension; +\fBWARNING:\fP each time \fBsed\fP/\fBcomment\fP/\fBuncomment\fP is called will +overwrite this backup +.UNINDENT +.sp Usage: .sp .nf @@ -19115,6 +20821,9 @@ The user to run gem as. NOTE: This modules is under heavy development and the API is subject to change. It may be replaced with a generic VCS module if this proves viable. .sp +Important, before using git over ssh, make sure your remote host fingerprint +exists in "~/.ssh/known_hosts" file. +.sp .nf .ft C https://github.com/saltstack/salt.git: @@ -19295,6 +21004,54 @@ Ensure that the specified kernel module is loaded The name of the kernel module to verify is loaded .UNINDENT .UNINDENT +.SS salt.states.module +.SS Execution of Salt modules from within states. +.sp +Individual module calls can be made via states. to call a single module +function use the run function. +.sp +One issue exists, since the name argument is present in the state call and is +present in many modules, this argument will need to be replaced in the sls +data with the argument m_name. +.INDENT 0.0 +.TP +.B salt.states.module.mod_watch(name, **kwargs) +Run a single module function +.INDENT 7.0 +.TP +.B \fBname\fP +The module function to execute +.TP +.B \fB**kwargs\fP +Pass any arguments needed to execute the function +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.states.module.run(name, **kwargs) +Run a single module function +.INDENT 7.0 +.TP +.B \fBname\fP +The module function to execute +.TP +.B \fB**kwargs\fP +Pass any arguments needed to execute the function +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.states.module.wait(name, **kwargs) +Run a single module function only if the watch statement calls it +.INDENT 7.0 +.TP +.B \fBname\fP +The module function to execute +.TP +.B \fB**kwargs\fP +Pass any arguments needed to execute the function +.UNINDENT +.UNINDENT .SS salt.states.mount .SS Mounting of filesystems. .sp @@ -19578,6 +21335,13 @@ eth3: \- type: slave \- master: bond0 +eth4: + network.managed: + \- enabled: True + \- type: eth + \- proto: dhcp + \- bridge: br0 + bond0: network.managed: \- type: bond @@ -19651,6 +21415,18 @@ bond0.12: \- network: bond0 \- require: \- network: bond0 +br0: + network.managed: + \- enabled: True + \- type: bridge + \- proto: dhcp + \- bridge: br0 + \- delay: 0 + \- bypassfirewall: True + \- use: + \- network: eth4 + \- require: + \- network: eth4 .ft P .fi .INDENT 0.0 @@ -19733,6 +21509,25 @@ None the pip executable or virtualenenv to use .UNINDENT .UNINDENT +.SS salt.states.pkgng +.SS Manage package remote repo using FreeBSD pkgng. +.sp +Salt can manage the url pkgng pulls packages from. +ATM the state and module are small so use cases are +typically rather simple: +.sp +.nf +.ft C +pkgng_clients: + pkgng: + \- update_packaging_site + \- name: "http://192.168.0.2" +.ft P +.fi +.INDENT 0.0 +.TP +.B salt.states.pkgng.update_packaging_site(name) +.UNINDENT .SS salt.states.pkg .SS Installation of packages using OS package managers such as yum or apt\-get. .sp @@ -19886,6 +21681,58 @@ The template database from which to build this database System user all operation should be preformed on behalf of .UNINDENT .UNINDENT +.SS salt.states.postgres_user +.SS Management of PostgreSQL users (roles). +.sp +The postgres_users module is used to create and manage Postgres users. +.sp +.nf +.ft C +frank: + postgres_user.present +.ft P +.fi +.INDENT 0.0 +.TP +.B salt.states.postgres_user.absent(name, runas=None) +Ensure that the named user is absent +.INDENT 7.0 +.TP +.B name +The username of the user to remove +.TP +.B runas +System user all operation should be preformed on behalf of +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B salt.states.postgres_user.present(name, createdb=False, createuser=False, encrypted=False, superuser=False, password=None, runas=None) +Ensure that the named user is present with the specified privileges +.INDENT 7.0 +.TP +.B name +The name of the user to manage +.TP +.B createdb +Is the user allowed to create databases? +.TP +.B createuser +Is the user allowed to create other users? +.TP +.B encrypted +Shold the password be encrypted in the system catalog? +.TP +.B superuser +Shold the new user be a "superuser" +.TP +.B password +The user\(aqs pasword +.TP +.B runas +System user all operation should be preformed on behalf of +.UNINDENT +.UNINDENT .SS salt.states.rvm .SS Managing Ruby installations and gemsets with Ruby Version Manager (RVM). .sp @@ -20038,7 +21885,7 @@ enforcing: selinux.mode samba_create_home_dirs: - selinx.boolean: + selinux.boolean: \- value: True \- persist: True .ft P @@ -20130,7 +21977,7 @@ The name of the init or rc script used to manage the service .UNINDENT .INDENT 0.0 .TP -.B salt.states.service.mod_watch(name, sig=None, reload=False) +.B salt.states.service.mod_watch(name, sig=None, reload=False, full_restart=False) The service watcher, called to invoke the watch command. .INDENT 7.0 .TP @@ -20381,7 +22228,7 @@ option to True to remove the user even if they are logged in .UNINDENT .INDENT 0.0 .TP -.B salt.states.user.present(name, uid=None, gid=None, groups=None, home=True, password=None, enforce_password=True, shell=None, fullname=None, roomnumber=None, workphone=None, homephone=None, other=None, unique=True, system=False) +.B salt.states.user.present(name, uid=None, gid=None, gid_from_name=False, groups=None, home=True, password=None, enforce_password=True, shell=None, fullname=None, roomnumber=None, workphone=None, homephone=None, other=None, unique=True, system=False) Ensure that the named user is present with the specified properties .INDENT 7.0 .TP @@ -20395,6 +22242,9 @@ will be assigned .B gid The default group id .TP +.B gid_from_name +If True, the default group id will be set to the id of the group with the same name as the user. +.TP .B groups A list of groups to assign the user to, pass a list object .TP @@ -20524,13 +22374,14 @@ Writing a renderer is easy, all that is required is that a Python module is placed in the rendered directory and that the module implements the render function. The render function will be passed the path of the SLS file. In the render function, parse the passed file and return the data structure -derived from the file. +derived from the file. You can place your custom renderers in a \fB_renderers\fP +directory in your file root (\fB/srv/salt/\fP). .SS Examples .sp The best place to find examples of renderers is in the Salt source code. The renderers included with Salt can be found here: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/renderers\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/renderers\fP .sp Here is a simple Jinja + YAML example: .sp @@ -20575,6 +22426,18 @@ Process json with the Mako templating engine T} _ T{ +\fBjson_wempy\fP +T} T{ +Process json with the Wempy templating engine +T} +_ +T{ +\fBpy\fP +T} T{ +Pure python state renderer +T} +_ +T{ \fByaml_jinja\fP T} T{ The default rendering engine, process yaml with the jinja2 templating engine @@ -20587,9 +22450,9 @@ Process yaml with the Mako templating engine T} _ T{ -\fBpy\fP +\fByaml_wempy\fP T} T{ -Pure python state renderer +Process yaml with the Wempy templating engine T} _ .TE @@ -20615,6 +22478,28 @@ high data format for salt states. .B salt.renderers.json_mako.render(template_file, env=\(aq\(aq, sls=\(aq\(aq) Render the data passing the functions and grains into the rendering system .UNINDENT +.SS salt.renderers.json_wempy +.sp +Process json with the Wempy templating engine +.sp +This renderer will take a json file with the Wempy template and render it to a +high data format for salt states. +.INDENT 0.0 +.TP +.B salt.renderers.json_wempy.render(template_file, env=\(aq\(aq, sls=\(aq\(aq) +Render the data passing the functions and grains into the rendering system +.UNINDENT +.SS salt.renderers.py +.sp +Pure python state renderer +.sp +The sls file should contain a function called \fBrun\fP which returns high state +data +.INDENT 0.0 +.TP +.B salt.renderers.py.render(template, env=\(aq\(aq, sls=\(aq\(aq) +Render the python module\(aqs components +.UNINDENT .SS salt.renderers.yaml_jinja .sp The default rendering engine, process yaml with the jinja2 templating engine @@ -20637,16 +22522,159 @@ high data format for salt states. .B salt.renderers.yaml_mako.render(template_file, env=\(aq\(aq, sls=\(aq\(aq) Render the data passing the functions and grains into the rendering system .UNINDENT -.SS salt.renderers.py +.SS salt.renderers.yaml_wempy .sp -Pure python state renderer +Process yaml with the Wempy templating engine .sp -The sls file should contain a function called \fBrun\fP which returns high state -data +This renderer will take a yaml file within a wempy template and render it to a +high data format for salt states. .INDENT 0.0 .TP -.B salt.renderers.py.render(template, env=\(aq\(aq, sls=\(aq\(aq) -Render the python module\(aqs components +.B salt.renderers.yaml_wempy.render(template_file, env=\(aq\(aq, sls=\(aq\(aq) +Render the data passing the functions and grains into the rendering system +.UNINDENT +.SH PILLARS +.sp +Salt includes a number of built\-in external pillars, listed at +\fIall\-salt.pillars\fP. +.sp +You may also wish to look at the standard pillar documentation, at +\fIpillar\-configuration\fP +.sp +The source for the built\-in Salt returners can be found here: +\fI\%https://github.com/saltstack/salt/blob/develop/salt/pillar\fP +.SH FULL LIST OF BUILTIN PILLARS +.TS +center; +|l|l|. +_ +T{ +\fBcmd_yaml\fP +T} T{ +Execute a command and read the output as YAML. The YAML data is then directly +T} +_ +T{ +\fBhiera\fP +T} T{ +Take in a hiera configuration file location and execute it. +T} +_ +T{ +\fBmongo\fP +T} T{ +Read pillar data from a mongodb collection. +T} +_ +.TE +.SS salt.pillar.cmd_yaml +.sp +Execute a command and read the output as YAML. The YAML data is then directly +overlaid onto the minion\(aqs pillar data +.INDENT 0.0 +.TP +.B salt.pillar.cmd_yaml.ext_pillar(command) +Execute a command and read the output as YAML +.UNINDENT +.SS salt.pillar.hiera +.sp +Take in a hiera configuration file location and execute it. +Adds the hiera data to pillar +.INDENT 0.0 +.TP +.B salt.pillar.hiera.ext_pillar(conf) +Execute hiera and return the data +.UNINDENT +.SS salt.pillar.mongo +.sp +Read pillar data from a mongodb collection. +.sp +This module will load a node\-specific pillar dictionary from a mongo +collection. It uses the node\(aqs id for lookups and can load either the whole +document, or just a specific field from that +document as the pillar dictionary. +.SS Salt Master Mongo Configuration +.sp +The module shares the same base mongo connection variables as +\fBsalt.returners.mongo_return\fP. These variables go in your master +config file. +.INDENT 0.0 +.INDENT 3.5 +.INDENT 0.0 +.IP \(bu 2 +\fBmongo.db\fP \- The mongo database to connect to. Defaults to \fB\(aqsalt\(aq\fP. +.IP \(bu 2 +\fBmongo.host\fP \- The mongo host to connect to. Supports replica sets by +specifying all hosts in the set, comma\-delimited. Defaults to \fB\(aqsalt\(aq\fP. +.IP \(bu 2 +\fBmongo.port\fP \- The port that the mongo database is running on. Defaults +to \fB27017\fP. +.IP \(bu 2 +\fBmongo.user\fP \- The username for connecting to mongo. Only required if +you are using mongo authentication. Defaults to \fB\(aq\(aq\fP. +.IP \(bu 2 +\fBmongo.password\fP \- The password for connecting to mongo. Only required +if you are using mongo authentication. Defaults to \fB\(aq\(aq\fP. +.UNINDENT +.UNINDENT +.UNINDENT +.SS Configuring the Mongo ext_pillar +.sp +The Mongo ext_pillar takes advantage of the fact that the Salt Master +configuration file is yaml. It uses a sub\-dictionary of values to adjust +specific features of the pillar. This is the explicit single\-line dictionary +notation for yaml. One may be able to get the easier\-to\-read multine dict to +work correctly with some experimentation. +.sp +.nf +.ft C +ext_pillar: + \- mongo: {collection: vm, id_field: name, re_pattern: \e.example\e.com, fields: [customer_id, software, apache_vhosts]} +.ft P +.fi +.sp +In the example above, we\(aqve decided to use the \fBvm\fP collection in the +database to store the data. Minion ids are stored in the \fBname\fP field on +documents in that collection. And, since minon ids are FQDNs in most cases, +we\(aqll need to trim the domain name in order to find the minon by hostname in +the collection. When we find a minion, return only the \fBcustomer_id\fP, +\fBsoftware\fP, and \fBapache_vhosts\fP fields, as that will contain the data we +want for a given node. They will be available directly inside the \fBpillar\fP +dict in your SLS templates. +.SS Module Documentation +.INDENT 0.0 +.TP +.B salt.pillar.mongo.ext_pillar(collection=\(aqpillar\(aq, id_field=\(aq_id\(aq, re_pattern=None, re_replace=\(aq\(aq, fields=None) +Connect to a mongo database and read per\-node pillar information. +.INDENT 7.0 +.TP +.B Parameters: +.INDENT 7.0 +.IP \(bu 2 +\fIcollection\fP: The mongodb collection to read data from. Defaults to +\fB\(aqpillar\(aq\fP. +.IP \(bu 2 +\fIid_field\fP: The field in the collection that represents an individual +minon id. Defaults to \fB\(aq_id\(aq\fP. +.IP \(bu 2 +\fIre_pattern\fP: If your naming convention in the collection is shorter +than the minion id, you can use this to trim the name. +\fIre_pattern\fP will be used to match the name, and \fIre_replace\fP will +be used to replace it. Backrefs are supported as they are in the +Python standard library. If \fBNone\fP, no mangling of the name will +be performed \- the collection will be searched with the entire +minion id. Defaults to \fBNone\fP. +.IP \(bu 2 +\fIre_replace\fP: Use as the replacement value in node ids matched with +\fIre_pattern\fP. Defaults to \(aq\(aq. Feel free to use backreferences here. +.IP \(bu 2 +\fIfields\fP: The specific fields in the document to use for the pillar +data. If \fBNone\fP, will use the entire document. If using the +entire document, the \fB_id\fP field will be converted to string. Be +careful with other fields in the document as they must be string +serializable. Defaults to \fBNone\fP. +.UNINDENT +.UNINDENT .UNINDENT .SH SALT RUNNERS .sp @@ -20676,7 +22704,7 @@ contains a function called \fBfoo\fP then the function could be called with: .sp The best examples of runners can be found in the Salt source: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/runners\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/runners\fP .sp A simple runner that returns a well\-formatted list of the minions that are responding to Salt calls would look like this: @@ -20702,7 +22730,7 @@ Salt 0.9.0 introduced the capability for Salt minions to publish commands. The intent of this feature is not for Salt minions to act as independent brokers one with another, but to allow Salt minions to pass commands to each other. .sp -In Salt 1.0 the ability to execute runners from the master was added. This +In Salt 0.10.0 the ability to execute runners from the master was added. This allows for the master to return collective data from runners back to the minions via the peer interface. .sp @@ -20813,6 +22841,48 @@ To execute the manage.up runner: # salt\-call publish.runner manage.up .ft P .fi +.SH CLIENT ACL SYSTEM +.sp +The salt client acl system is a means to allow system users other than root to +have access to execute select salt commands on minions from the master. +.sp +The client acl system is configured in the master configuration file via the +\fBclient_acl\fP configuration option. Under the \fBclient_acl\fP configuration +option the users open to send commands are specified and then a list of regular +expressions which specify the minion functions which will be made available to +specified user. This configuration is much like the \fBpeer\fP configuration: +.sp +.nf +.ft C +# Allow thatch to execute anything and allow fred to use ping and pkg +client_acl: + thatch: + \- .* + fred: + \- ping.* + \- pkg.* +.ft P +.fi +.SS Permission Issues +.sp +Directories required for \fBclient_acl\fP must be modified to be readable by the +users specified: +.sp +.nf +.ft C +chmod 755 /var/cache/salt /var/cache/salt/jobs /var/run/salt +.ft P +.fi +.sp +If you are upgrading from earlier versions of salt you must also remove any +existing user keys and re\-start the Salt master: +.sp +.nf +.ft C +rm /var/cache/salt/.*keys +service salt\-master restart +.ft P +.fi .SH SALT SYNDIC .sp The Salt Syndic interface is a powerful tool which allows for the construction @@ -21409,22 +23479,50 @@ will sync all module types over to a minion. For more information see: # The address of the interface to bind to #interface: 0.0.0.0 -# The port used by the publisher +# The tcp port used by the publisher #publish_port: 4505 -# The user to run salt +# Refresh the publisher connections when sending out commands, this is a fix +# for zeromq losing some minion connections. Default: True +#pub_refresh: True + +# The user to run the salt\-master as. Salt will update all permissions to +# allow the specified user to run the master. If the modified files cause +# conflicts set verify_env to False. #user: root +# Max open files +# Each minion connecting to the master uses AT LEAST one file descriptor, the +# master subscription connection. If enough minions connect you might start +# seeing on the console(and then salt\-master crashes): +# Too many open files (tcp_listener.cpp:335) +# Aborted (core dumped) +# +# By default this value will be the one of \(gaulimit \-Hn\(ga, ie, the hard limit for +# max open files. +# +# If you wish to set a different value than the default one, uncomment and +# configure this setting. Remember that this value CANNOT be higher than the +# hard limit. Raising the hard limit depends on your OS and/or distribution, +# a good way to find the limit is to search the internet for(for example): +# raise max open files hard limit debian +# +#max_open_files: 100000 + # The number of worker threads to start, these threads are used to manage # return calls made from minions to the master, if the master seems to be # running slowly, increase the number of threads #worker_threads: 5 -# The port used by the communication interface +# The port used by the communication interface. The ret (return) port is the +# interface used for the file server, authentication, job returnes, etc. #ret_port: 4506 +# Specify the location of the daemon process ID file +#pidfile: /var/run/salt\-master.pid + # The root directory prepended to these options: pki_dir, cachedir, -# sock_dir, log_file, autosign_file. +# sock_dir, log_file, autosign_file, extension_modules #root_dir: / # Directory used to store public key data @@ -21433,7 +23531,10 @@ will sync all module types over to a minion. For more information see: # Directory to store job and cache data #cachedir: /var/cache/salt -# Set the number of hours to keep old job information +# Verify and set permissions on configuration directories at startup +#verify_env: True + +# Set the number of hours to keep old job information in the job cache #keep_jobs: 24 # Set the default timeout for the salt command and api, the default is 5 @@ -21441,7 +23542,7 @@ will sync all module types over to a minion. For more information see: #timeout: 5 # Set the directory used to hold unix sockets -#sock_dir: /tmp/salt\-unix +#sock_dir: /var/run/salt # The master maintains a job cache, while this is a great addition it can be # a burden on the master for larger deployments (over 5000 minions). @@ -21450,6 +23551,9 @@ will sync all module types over to a minion. For more information see: # #job_cache: True +# Cache minion grains and pillar data in the cachedir. +#minion_data_cache: True + # Set the acceptance level for serialization of messages. This should only be # set if the master is newer than 0.9.5 and the minion are older. This option # allows a 0.9.5 and newer master to communicate with minions 0.9.4 and @@ -21500,6 +23604,16 @@ will sync all module types over to a minion. For more information see: # If an autosign_file is specified permissive access will allow group access # to that specific file. #permissive_pki_access: False +# +# Allow users on the master access to execute specific commands on minions. +# This setting should be treated with care since it opens up execution +# capabilities to non root users. By default this capability is completely +# disabled. +# +# client_acl: +# larry: +# \- test.ping +# \- network.* ##### Master Module Management ##### @@ -21509,8 +23623,9 @@ will sync all module types over to a minion. For more information see: # Add any additional locations to look for master runners #runner_dirs: [] # -#Enable Cython for master side modules +# Enable Cython for master side modules #cython_enable: False +# ##### State System settings ##### ########################################## @@ -21531,6 +23646,17 @@ will sync all module types over to a minion. For more information see: # The failhard option tells the minions to stop immediately after the first # failure detected in the state execution, defaults to False #failhard: False +# +# The state_verbose and state_output settings can be used to change the way +# state system data is printed to the display. By default all data is printed. +# The state_verbose setting can be set to True or False, when set to False +# all data that has a result of True and no changes will be suppressed. +#state_verbose: True +# +# The state_output setting changes if the output is the full multi line +# output for each changed state if set to \(aqfull\(aq, but if set to \(aqterse\(aq +# the output will be shortened to a single line. +#state_output: full ##### File Server settings ##### ########################################## @@ -21580,7 +23706,7 @@ will sync all module types over to a minion. For more information see: # #ext_pillar: # \- hiera: /etc/hiera.yaml -# \- cmd: cat /etc/salt/yaml +# \- cmd_yaml: cat /etc/salt/yaml # ##### Syndic settings ##### @@ -21642,29 +23768,11 @@ will sync all module types over to a minion. For more information see: # \- manage.up # -##### Cluster settings ##### -########################################## -# Salt supports automatic clustering, salt creates a single ip address which -# is shared among the individual salt components using ucarp. The private key -# and all of the minion keys are maintained across the defined cluster masters. -# The failover service is automatically managed via these settings - -# List the identifiers for the other cluster masters in this manner: -# [saltmaster\-01.foo.com,saltmaster\-02.foo.com,saltmaster\-03.foo.com] -# The members of this master array must be running as salt minions to -# facilitate the distribution of cluster information -#cluster_masters: [] - -# The cluster modes are "paranoid" and "full" -# paranoid will only distribute the accepted minion public keys. -# full will also distribute the master private key. -#cluster_mode: paranoid - - ##### Logging settings ##### ########################################## # The location of the master log file #log_file: /var/log/salt/master +#key_logfile: /var/log/salt/key # # The level of messages to send to the log file. # One of \(aqgarbage\(aq, \(aqtrace\(aq, \(aqdebug\(aq, info\(aq, \(aqwarning\(aq, \(aqerror\(aq, \(aqcritical\(aq. @@ -21675,7 +23783,7 @@ will sync all module types over to a minion. For more information see: # The date and time format used in log messages. Allowed date/time formating # can be seen here: # http://docs.python.org/library/time.html#time.strftime -#log_datefmt: \(aq%H:%M:%S\(aq +#log_datefmt: \(aq%Y\-%m\-%d %H:%M:%S\(aq # # The format of the console logging messages. Allowed formatting options can # be seen here: @@ -21732,6 +23840,9 @@ will sync all module types over to a minion. For more information see: # The user to run salt #user: root +# Specify the location of the daemon process ID file +#pidfile: /var/run/salt\-minion.pid + # The root directory prepended to these options: pki_dir, cachedir, log_file. #root_dir: / @@ -21750,6 +23861,17 @@ will sync all module types over to a minion. For more information see: # FQDN (for instance, Solaris). #append_domain: +# Custom static grains for this minion can be specified here and used in SLS +# files just like all other grains. This example sets 4 custom grains, with +# the \(aqroles\(aq grain having two values that can be matched against: +#grains: +# roles: +# \- webserver +# \- memcache +# deployment: datacenter4 +# cabinet: 13 +# cab_u: 14\-15 + # If the connection to the server is interrupted, the minion will # attempt to reconnect. sub_timeout allows you to control the rate # of reconnection attempts (in seconds). To disable reconnects, set @@ -21759,22 +23881,48 @@ will sync all module types over to a minion. For more information see: # Where cache data goes #cachedir: /var/cache/salt +# Verify and set permissions on configuration directories at startup +#verify_env: True + # The minion can locally cache the return data from jobs sent to it, this # can be a good way to keep track of jobs the minion has executed # (on the minion side). By default this feature is disabled, to enable # set cache_jobs to True #cache_jobs: False +# set the directory used to hold unix sockets +#sock_dir: /var/run/salt + +# Backup files that are replaced by file.managed and file.recurse under +# \(aqcachedir\(aq/file_backups relative to their original location and appended +# with a timestamp. The only valid setting is "minion". Disabled by default. +# +# Alternatively this can be specified for each file in state files: +# +# /etc/ssh/sshd_config: +# file.managed: +# \- source: salt://ssh/sshd_config +# \- backup: minion +# +#backup_mode: minion + # When waiting for a master to accept the minion\(aqs public key, salt will # continuously attempt to reconnect until successful. This is the time, in # seconds, between those reconnection attempts. -#acceptance_wait_time = 10 +#acceptance_wait_time: 10 # When healing a dns_check is run, this is to make sure that the originally # resolved dns has not changed, if this is something that does not happen in # your environment then set this value to False. #dns_check: True +# Windows platforms lack posix IPC and must rely on slower TCP based inter\- +# process communications. Set ipc_mode to \(aqtcp\(aq on such systems +#ipc_mode: ipc +# +# Overwrite the default tcp ports used by the minion when in tcp mode +#tcp_pub_port: 4510 +#tcp_pull_port: 4511 # The minion can include configuration from other files. To enable this, # pass a list of paths to this option. The paths can be either relative or @@ -21785,12 +23933,12 @@ will sync all module types over to a minion. For more information see: # # # Include a config file from some other path: -#include: /etc/salt/extra_config +# include: /etc/salt/extra_config # # Include config from several files and directories: -#include: -# \- /etc/salt/extra_config -# \- /etc/roles/webserver +# include: +# \- /etc/salt/extra_config +# \- /etc/roles/webserver ##### Minion module management ##### ########################################## @@ -21818,6 +23966,7 @@ will sync all module types over to a minion. For more information see: # # Enable Cython modules searching and loading. (Default: False) #cython_enable: False +# ##### State Management Settings ##### ########################################### @@ -21833,6 +23982,10 @@ will sync all module types over to a minion. For more information see: # #renderer: yaml_jinja # +# The failhard option tells the minions to stop immediately after the first +# failure detected in the state execution, defaults to False +#failhard: False +# # state_verbose allows for the data returned from the minion to be more # verbose. Normally only states that fail or states that have changes are # returned, but setting state_verbose to True will return all states that @@ -21859,6 +24012,20 @@ will sync all module types over to a minion. For more information see: # If using the local file directory, then the state top file name needs to be # defined, by default this is top.sls. #state_top: top.sls +# +# Run states when the minion daemon starts. To enable, set startup_states to: +# \(aqhighstate\(aq \-\- Execute state.highstate +# \(aqsls\(aq \-\- Read in the sls_list option and execute the named sls files +# \(aqtop\(aq \-\- Read top_file option and execute based on that file on the Master +#startup_states: \(aq\(aq +# +# list of states to run when the minion starts up if startup_states is \(aqsls\(aq +#sls_list: +# \- edit.vim +# \- hyper +# +# top file to execute if startup_states is \(aqtop\(aq +#top_file: \(aq\(aq ##### File Directory Settings ##### ########################################## @@ -21918,6 +24085,21 @@ will sync all module types over to a minion. For more information see: # you\(aqve given access to. This is potentially quite insecure. #permissive_pki_access: False +# The state_verbose and state_output settings can be used to change the way +# state system data is printed to the display. By default all data is printed. +# The state_verbose setting can be set to True or False, when set to False +# all data that has a result of True and no changes will be suppressed. +#state_verbose: True +# +# The state_output setting changes if the output is the full multi line +# output for each changed state if set to \(aqfull\(aq, but if set to \(aqterse\(aq +# the output will be shortened to a single line. +#state_output: full +# +# Fingerprint of the master public key to double verify the master is valid, +# the master fingerprint can be found by running "salt\-key \-F master" on the +# salt master. +#master_finger: \(aq\(aq ###### Thread settings ##### ########################################### @@ -21938,7 +24120,7 @@ will sync all module types over to a minion. For more information see: # # The date and time format used in log messages. Allowed date/time formating # can be seen on http://docs.python.org/library/time.html#time.strftime -#log_datefmt: \(aq%H:%M:%S\(aq +#log_datefmt: \(aq%Y\-%m\-%d %H:%M:%S\(aq # # The format of the console logging messages. Allowed formatting options can # be seen on http://docs.python.org/library/logging.html#logrecord\-attributes @@ -21963,6 +24145,9 @@ will sync all module types over to a minion. For more information see: # the module name is followed by a . and then the value. Also, all top level # data must be applied via the yaml dict construct, some examples: # +# You can specify that all modules should run in test mode: +#test: True +# # A simple value for the test module: #test.foo: foo # @@ -21972,6 +24157,19 @@ will sync all module types over to a minion. For more information see: # A dict for the test module: #test.baz: {spam: sausage, cheese: bread} + +###### Update settings ###### +########################################### +# Using the features in Esky, a salt minion can both run as a frozen app and +# be updated on the fly. These options control how the update process +# (saltutil.update()) behaves. +# +# The url for finding and downloading updates. Disabled by default. +#update_url: False +# +# The list of services to restart after a successful update. Empty by default. +#update_restart_services: [] + .ft P .fi .SH CONFIGURING THE SALT MASTER @@ -22132,6 +24330,28 @@ public keys from the minions auto_accept: False .ft P .fi +.SS \fBautosign_file\fP +.sp +Default \fBnot defined\fP +.sp +If the autosign_file is specified incoming keys specified in +the autosign_file will be automatically accepted. Regular expressions as +well as globbing can be used. This is insecure! +.SS \fBclient_acl\fP +.sp +Default: {} +.sp +Enable user accounts on the master to execute specific modules. These modules +can be expressed as regular expressions +.sp +.nf +.ft C +client_acl: + fred: + \- test.ping + \- pkg.* +.ft P +.fi .SS Master Module Management .SS \fBrunner_dirs\fP .sp @@ -22312,9 +24532,11 @@ Default:: \fBNone\fP .ft C ext_pillar: \- hiera: /etc/hiera.yaml - \- cmd: cat /etc/salt/yaml + \- cmd_yaml: cat /etc/salt/yaml .ft P .fi +.sp +There are additional details at \fIsalt\-pillars\fP .SS Syndic Server Settings .sp A Salt syndic is a Salt master used to pass commands from a higher Salt master to @@ -22454,6 +24676,14 @@ log_granular_levels: \(aqsalt.modules\(aq: \(aqdebug\(aq .ft P .fi +.SS \fBdefault_include\fP +.sp +Default: \fBmaster.d/*.conf\fP +.sp +The minion can include configuration from other files. Per default the +minion will automatically include all config files from \fImaster.d/*.conf\fP +where minion.d is relative to the directory of the minion configuration +file. .SH CONFIGURING THE SALT MINION .sp The Salt system is amazingly simple and easy to configure, the two components @@ -22550,6 +24780,17 @@ The location for minion cache data. cachedir: /var/cache/salt .ft P .fi +.SS \fBbackup_mode\fP +.sp +Default: \fB[]\fP +.sp +Backup files replaced by file.managed and file.recurse under cachedir. +.sp +.nf +.ft C +backup_mode: minion +.ft P +.fi .SS \fBcache_jobs\fP .sp Default: \fBFalse\fP @@ -22798,6 +25039,14 @@ log_granular_levels: \(aqsalt.modules\(aq: \(aqdebug\(aq .ft P .fi +.SS \fBdefault_include\fP +.sp +Default: \fBminion.d/*.conf\fP +.sp +The minion can include configuration from other files. Per default the +minion will automatically include all config files from \fIminion.d/*.conf\fP +where minion.d is relative to the directory of the minion configuration +file. .SS \fBinclude\fP .sp Default: \fBnot defined\fP @@ -22825,6 +25074,36 @@ include: \- /etc/roles/webserver .ft P .fi +.SS Frozen Build Update Settings +.sp +These options control how \fBsalt.modules.saltutil.update()\fP works with esky +frozen apps. For more information look at \fI\%https://github.com/cloudmatrix/esky/\fP. +.SS \fBupdate_url\fP +.sp +Default: \fBFalse\fP (Update feature is disabled) +.sp +The url to use when looking for application updates. Esky depends on directory +listings to search for new versions. A webserver running on your Master is a +good starting point for most setups. +.sp +.nf +.ft C +update_url: \(aqhttp://salt.example.com/minion\-updates\(aq +.ft P +.fi +.SS \fBupdate_restart_services\fP +.sp +Default: \fB[]\fP (service restarting on update is disabled) +.sp +A list of services to restart when the minion software is updated. This would +typically just be a list containing the minion\(aqs service name, but you may +have other services that need to go with it. +.sp +.nf +.ft C +update_restart_services: [\(aqsalt\-minion\(aq] +.ft P +.fi .SH COMMAND LINE REFERENCE .sp Salt can be controlled by a command line client by the root user on the Salt @@ -23110,7 +25389,7 @@ file. .TP .B \-\-return Chose an alternative returner to call on the minion, if an alternative -returner is used then the return will not come back tot he command line +returner is used then the return will not come back to the command line but will be sent to the specified return system. .UNINDENT .INDENT 0.0 @@ -23316,8 +25595,8 @@ Delete the named minion key for command execution. .UNINDENT .INDENT 0.0 .TP -.B \-D DELETE_ALL, \-\-delete\-all=DELETE_ALL -Deleta all keys +.B \-D, \-\-delete\-all +Delete all keys .UNINDENT .INDENT 0.0 .TP @@ -23326,6 +25605,50 @@ The master configuration file needs to be read to determine where the Salt keys are stored via the pki_dir configuration value; default=/etc/salt/master .UNINDENT +.INDENT 0.0 +.TP +.B \-p PRINT, \-\-print=PRINT +Print the specified public key +.UNINDENT +.INDENT 0.0 +.TP +.B \-P, \-\-print\-all +Print all public keys +.UNINDENT +.INDENT 0.0 +.TP +.B \-q, \-\-quiet +Supress output +.UNINDENT +.INDENT 0.0 +.TP +.B \-y, \-\-yes +Answer \(aqYes\(aq to all questions presented, defaults to False +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-key\-logfile=KEY_LOGFILE +Send all output to a file. Default is /var/log/salt/key +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-gen\-keys=GEN_KEYS +Set a name to generate a keypair for use with salt +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-gen\-keys\-dir=GEN_KEYS_DIR +Set the directory to save the generated keypair. Only works +with \(aqgen_keys_dir\(aq option; default is the current directory. +.UNINDENT +.INDENT 0.0 +.TP +.B \-\-keysize=KEYSIZE +Set the keysize for the generated key, only works with +the \(aq\-\-gen\-keys\(aq option, the key size must be 2048 or +higher, otherwise it will be rounded up to 2048. The +default is 2048. +.UNINDENT .SH SALT-CP .sp Copy a file to a set of systems @@ -23420,6 +25743,10 @@ default=/etc/salt/master salt\-call [options] .ft P .fi +.SS Description +.sp +The salt\-call command is used to run module functions locally on a minion +instead of executing them from the master. .SS Options .INDENT 0.0 .TP @@ -23631,9 +25958,9 @@ header. .SS Source Files Implimenting Components .sp The pubkey authentication is managed via the salt.master module: -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/master.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/master.py\fP The regular minion authentication is managed via the salt.crypt module: -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/crypt.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/crypt.py\fP The salt.crypt module contains a class "SAuth" that can be used for standalone authentication with the Salt master, this is most likely the best place to start when looking into how the authentication mechanism works @@ -23761,7 +26088,7 @@ but when enabled the cache is used to make grain matching from the salt command more powerful, since the minions that will match can be pre determined. .SS Backup Files .sp -By default all files replaced by the file.managed and file.recuse states we +By default all files replaced by the file.managed and file.recurse states we simply deleted. 0.10.2 adds a new option. By setting the backup option to \fBminion\fP the files are backed up before they are replaced. .sp @@ -23828,7 +26155,7 @@ Basic support for controlling nzbget by Joseph Hall .SS Bluetooth .sp Baisc \fBbluez\fP support for managing and controlling Bluetooth devices. -Supports scanning as well as pairing/unpairing. +Supports scanning as well as pairing/unpairing by Joseph Hall. .SS Test Updates .SS Consistency Testing .sp @@ -23847,6 +26174,172 @@ at the closed tickets for 0.10.2, this is a very substantial update: As Salt deployments grow new ways to break Salt are discovered. 0.10.2 comes with a number of fixes for the minions and master greatly improving Salt stability. +.SS Salt 0.10.3 Release Notes +.sp +The latest taste of Salt has come, this release has many fixes and feature +additions. Modifications have been made to make ZeroMQ connections more +reliable, the begining of the ACL system is in place, a new command line +parsing system has been added, dynamic module distribution has become more +environment aware, the new \fImaster_finger\fP option and many more! +.SS Major Features +.SS ACL System +.sp +The new ACL system has been introduced. The ACL system allows for system users +other than root to execute salt commands. Users can be allowed to execute +specific commands in the same way that minions are opened up to the peer +system. +.sp +The configuration value to open up the ACL system is called \fBclient_acl\fP +and is configured like so: +.sp +.nf +.ft C +client_acl: + fred: + \- test..* + \- pkg.list_pkgs +.ft P +.fi +.sp +Where \fIfred\fP is allowed access to functions in the test module and to the +\fBpkg.list_pkgs\fP function. +.SS Master Finger Option +.sp +The \fImaster_finger\fP option has been added to improve the security of minion +provisioning. The \fImaster_finger\fP option allows for the fingerprint of the +master public key to be set in the configuration file to double verify that the +master is valid. This option was added in response to a motivation to pre +authenticate the master when provisioning new minions to help prevent +man in the middle attacks in some situations. +.SS Salt Key Fingerprint Generation +.sp +The ability to generate fingerprints of keys used by Salt has been added to +\fBsalt\-key\fP. The new option \fIfinger\fP accepts the name of the key to generate +and display a fingerprint for. +.sp +.nf +.ft C +salt\-key \-F master +.ft P +.fi +.sp +Will display the fingerprints for the master public and private keys. +.SS Parsing System +.sp +Pedro Algavio, aka s0undt3ch, has added a substantial update to the command +line parsing system that makes the help message output much cleaner and easier +to search through. Salt parsers now have \fI\-\-versions\-report\fP besides usual +\fI\-\-version\fP info which you can provide when reporting any issues found. +.SS Key Generation +.sp +We have reduced the requirements needed for \fIsalt\-key\fP to generate minion keys. +You\(aqre no longer required to have salt configured and it\(aqs common directories +created just to generate keys. This might prove useful if you\(aqre batch creating +keys to pre\-load on minions. +.SS Startup States +.sp +A few configuration options have been added which allow for states to be run +when the minion daemon starts. This can be a great advantage when deploying +with Salt because the minion can apply states right when it first runs. To +use startup states set the \fBstartup_states\fP configuration option on the +minion to \fIhighstate\fP. +.SS New Exclude Declaration +.sp +Some users have asked about adding the ability to ensure that other sls files +or ids are excluded from a state run. The exclude statement will delete all of +the data loaded from the specified sls file or will delete the specified id: +.sp +.nf +.ft C +exclude: + \- sls: http + \- id: /etc/vimrc +.ft P +.fi +.SS Max Open Files +.sp +While we\(aqre currently unable to properly handle ZeroMQ\(aqs abort signals when the +max open files is reached, due to the way that\(aqs handled on ZeroMQ\(aqs, we have +minimized the chances of this happening without at least warning the user. +.SS More State Output Options +.sp +Some major changes have been made to the state output system. In the past state +return data was printed in a very verbose fashion and only states that failed +or made changes were printed by default. Now two options can be passed to the +master and minion configuration files to change the behavior of the state +output. State output can be set to verbose (default) or non\-verbose with the +\fBstate_verbose\fP option: +.sp +.nf +.ft C +state_verbose: False +.ft P +.fi +.sp +It is noteworthy that the state_verbose option used to be set to \fIFalse\fP by +default but has been changed to \fITrue\fP by default in 0.10.3 due to many +requests for the change. +.sp +Te next option to be aware of new and called \fBstate_output\fP. This option +allows for the state output to be set to \fIfull\fP (default) or \fIterse\fP. +.sp +The \fIfull\fP output is the standard state output, but the new \fIterse\fP output +will print only one line per state making the output much easier to follow when +executing a large state system. +.sp +.nf +.ft C +state_output: terse +.ft P +.fi +.SS \fIstate.file.append\fP Improvements +.sp +The salt state \fIfile.append()\fP tries \fInot\fP to append existing text. Previously +the matching check was being made line by line. While this kind of check might +be enough for most cases, if the text being appended was multi\-line, the check +would not work properly. This issue is now properly handled, the match is done +as a whole ignoring any white space addition or removal except inside commas. +For those thinking that, in order to properly match over multiple lines, salt +will load the whole file into memory, that\(aqs not true. For most cases this is +not important but an erroneous order to read a 4GB file, if not properly +handled, like salt does, could make salt chew that amount of memory. Salt has +a buffered file reader which will keep in memory a maximum of 256KB and +iterates over the file in chunks of 32KB to test for the match, more than +enough, if not, explain your usage on a ticket. With this change, also +\fIsalt.modules.file.contains()\fP, \fIsalt.modules.file.contains_regex()\fP, +\fIsalt.modules.file.contains_glob()\fP and \fIsalt.utils.find\fP now do the searching +and/or matching using the buffered chunks approach explained above. +.sp +Two new keyword arguments were also added, \fImakedirs\fP and \fIsource\fP. +The first, \fImakedirs\fP will create the necessary directories in order to append +to the specified file, of course, it only applies if we\(aqre trying to append to +a non\-existing file on a non\-existing directory: +.sp +.nf +.ft C +/tmp/salttest/file\-append\-makedirs: + file.append: + text: foo + makedirs: True +.ft P +.fi +.sp +The second, \fIsource\fP, allows to append the contents of a file instead of +specifying the text. +.sp +.nf +.ft C +/tmp/salttest/file\-append\-source: + +file.append: + \- source: salt://testfile +.ft P +.fi +.SS Security Fix +.sp +A timing vulnerability was uncovered in the code which decrypts the AES +messages sent over the network. This has been fixed and upgrading is +strongly recommended. .SS Salt 0.6.0 release notes .sp The Salt remote execution manager has reached initial functionality! Salt is a @@ -24040,7 +26533,7 @@ use the file extension “.pyx” and the minion module will be compiled when the minion is started. An example cython module is included in the main distribution called cytest.pyx: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules/cytest.pyx\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules/cytest.pyx\fP .SS Dynamic Returners \- .sp By default salt returns command data back to the salt master, but now salt can @@ -24054,7 +26547,7 @@ data so anything from MySQL, redis, mongodb and more! .sp There are 2 simple stock returners in the returners directory: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/returners\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/returners\fP .sp The documentation on writing returners will be added to the wiki shortly, and returners can be written in pure Python, or in cython. @@ -24070,7 +26563,7 @@ Information on how to use this simple addition has been added to the wiki: The test module has an example of using the __opts__ dict, and how to set default options: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/modules/test.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/modules/test.py\fP .SS Advanced Minion Threading: .sp In 0.7.0 the minion would block after receiving a command from the master, now @@ -24082,7 +26575,7 @@ exploit the negative aspects of the Python GIL to run faster and more reliably, but simple calls will still be faster with Python threading. The configuration option can be found in the minion configuration file: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/conf/minion\fP +\fI\%https://github.com/saltstack/salt/blob/develop/conf/minion\fP .sp Lowered Supported Python to 2.6 \- .sp @@ -24160,7 +26653,7 @@ The system for loading salt modules has been pulled out of the minion class to be a standalone module, this has enabled more dynamic loading of Salt modules and enables many of the updates in 0.8.7 – .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/loader.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/loader.py\fP .sp Salt Job ids are now microsecond precise, this was needed to repair a race condition unveiled by the speed improvements in the new ZeroMQ topology. @@ -24444,7 +26937,7 @@ The minion and master classes have been redesigned to allow for specialized minion and master servers to be easily created. An example on how this is done for the master can be found in the \fBmaster.py\fP salt module: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/master.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/master.py\fP .sp The \fBMaster\fP class extends the \fBSMaster\fP class and set up the main master server. @@ -24452,7 +26945,7 @@ server. The minion functions can now also be easily added to another application via the \fBSMinion\fP class, this class can be found in the \fBminion.py\fP module: .sp -\fI\%https://github.com/saltstack/salt/blob/v0.10.2/salt/minion.py\fP +\fI\%https://github.com/saltstack/salt/blob/develop/salt/minion.py\fP .SS Cleaner Key Management .sp This release changes some of the key naming to allow for multiple master keys @@ -24747,7 +27240,7 @@ Make a symlink. .B file.recurse The recurse state function will recursively download a directory on the master file server and place it on the minion. Any change in the files on -the master will be pushed to the minion. The recuse function is very +the master will be pushed to the minion. The recurse function is very powerful and has been tested by pushing out the full Linux kernel source. .sp .nf @@ -26201,4 +28694,5 @@ Thomas S. Hatch and many others, please see the Authors fil .SH COPYRIGHT 2012, Thomas S. Hatch .\" Generated by docutils manpage writer. +.\" . diff --git a/doc/ref/cli/salt-call.rst b/doc/ref/cli/salt-call.rst index 8d0a2b4924..0c325ac794 100644 --- a/doc/ref/cli/salt-call.rst +++ b/doc/ref/cli/salt-call.rst @@ -9,6 +9,12 @@ Synopsis salt-call [options] +Description +=========== + +The salt-call command is used to run module functions locally on a minion +instead of executing them from the master. + Options ======= diff --git a/doc/ref/cli/salt-key.rst b/doc/ref/cli/salt-key.rst index a272c4081f..f1f5e51b03 100644 --- a/doc/ref/cli/salt-key.rst +++ b/doc/ref/cli/salt-key.rst @@ -51,12 +51,48 @@ Options Delete the named minion key for command execution. -.. option:: -D DELETE_ALL, --delete-all=DELETE_ALL +.. option:: -D, --delete-all - Deleta all keys + Delete all keys .. option:: -c CONFIG, --config=CONFIG The master configuration file needs to be read to determine where the Salt keys are stored via the pki_dir configuration value; default=/etc/salt/master + +.. option:: -p PRINT, --print=PRINT + + Print the specified public key + +.. option:: -P, --print-all + + Print all public keys + +.. option:: -q, --quiet + + Supress output + +.. option:: -y, --yes + + Answer 'Yes' to all questions presented, defaults to False + +.. option:: --key-logfile=KEY_LOGFILE + + Send all output to a file. Default is /var/log/salt/key + +.. option:: --gen-keys=GEN_KEYS + + Set a name to generate a keypair for use with salt + +.. option:: --gen-keys-dir=GEN_KEYS_DIR + + Set the directory to save the generated keypair. Only works + with 'gen_keys_dir' option; default is the current directory. + +.. option:: --keysize=KEYSIZE + + Set the keysize for the generated key, only works with + the '--gen-keys' option, the key size must be 2048 or + higher, otherwise it will be rounded up to 2048. The + default is 2048. diff --git a/doc/ref/cli/salt.rst b/doc/ref/cli/salt.rst index 9b86971a80..0b2b9114d3 100644 --- a/doc/ref/cli/salt.rst +++ b/doc/ref/cli/salt.rst @@ -108,7 +108,7 @@ Options .. option:: --return Chose an alternative returner to call on the minion, if an alternative - returner is used then the return will not come back tot he command line + returner is used then the return will not come back to the command line but will be sent to the specified return system. .. option:: -Q, --query diff --git a/doc/ref/clientacl.rst b/doc/ref/clientacl.rst new file mode 100644 index 0000000000..4afe0bf4e8 --- /dev/null +++ b/doc/ref/clientacl.rst @@ -0,0 +1,40 @@ +================= +Client ACL system +================= + +The salt client acl system is a means to allow system users other than root to +have access to execute select salt commands on minions from the master. + +The client acl system is configured in the master configuration file via the +``client_acl`` configuration option. Under the ``client_acl`` configuration +option the users open to send commands are specified and then a list of regular +expressions which specify the minion functions which will be made available to +specified user. This configuration is much like the ``peer`` configuration: + +.. code-block:: yaml + + # Allow thatch to execute anything and allow fred to use ping and pkg + client_acl: + thatch: + - .* + fred: + - ping.* + - pkg.* + +Permission Issues +================= + +Directories required for ``client_acl`` must be modified to be readable by the +users specified: + +.. code-block:: bash + + chmod 755 /var/cache/salt /var/cache/salt/jobs /var/run/salt + +If you are upgrading from earlier versions of salt you must also remove any +existing user keys and re-start the Salt master: + +.. code-block:: bash + + rm /var/cache/salt/.*keys + service salt-master restart diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 9a936934c9..c98cd930d8 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -194,6 +194,34 @@ public keys from the minions auto_accept: False +.. conf_master:: autosign_file + +``autosign_file`` +----------------- + +Default ``not defined`` + +If the autosign_file is specified incoming keys specified in +the autosign_file will be automatically accepted. Regular expressions as +well as globbing can be used. This is insecure! + +.. conf_master:: client_acl + +``client_acl`` +-------------- + +Default: {} + +Enable user accounts on the master to execute specific modules. These modules +can be expressed as regular expressions + +.. code-block:: yaml + + client_acl: + fred: + - test.ping + - pkg.* + Master Module Management ------------------------ @@ -362,6 +390,8 @@ The buffer size in the file server in bytes file_buffer_size: 1048576 +.. _pillar-configuration: + Pillar Configuration -------------------- @@ -406,8 +436,9 @@ Default:: ``None`` ext_pillar: - hiera: /etc/hiera.yaml - - cmd: cat /etc/salt/yaml + - cmd_yaml: cat /etc/salt/yaml +There are additional details at :ref:`salt-pillars` Syndic Server Settings ---------------------- @@ -568,3 +599,13 @@ still wish to have 'salt.modules' at the 'debug' level: log_granular_levels: 'salt': 'warning', 'salt.modules': 'debug' + +``default_include`` +------------------- + +Default: ``master.d/*.conf`` + +The minion can include configuration from other files. Per default the +minion will automatically include all config files from `master.d/*.conf` +where minion.d is relative to the directory of the minion configuration +file. diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index c1c771fe66..18deb96f65 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -112,6 +112,19 @@ The location for minion cache data. cachedir: /var/cache/salt +.. conf_minion:: cachedir + +``backup_mode`` +--------------- + +Default: ``[]`` + +Backup files replaced by file.managed and file.recurse under cachedir. + +.. code-block:: yaml + + backup_mode: minion + .. conf_minion:: cache_jobs ``cache_jobs`` @@ -407,6 +420,16 @@ still wish to have 'salt.modules' at the 'debug' level: .. conf_minion:: include +``default_include`` +------------------- + +Default: ``minion.d/*.conf`` + +The minion can include configuration from other files. Per default the +minion will automatically include all config files from `minion.d/*.conf` +where minion.d is relative to the directory of the minion configuration +file. + ``include`` ----------- @@ -415,12 +438,12 @@ Default: ``not defined`` The minion can include configuration from other files. To enable this, pass a list of paths to this option. The paths can be either relative or absolute; if relative, they are considered to be relative to the directory -the main minion configuration file lives in. Paths can make use of +the main minion configuration file lives in. Paths can make use of shell-style globbing. If no files are matched by a path passed to this option then the minion will log a warning message. .. code-block:: yaml - + # Include files from a minion.d directory in the same # directory as the minion config file include: minion.d/* @@ -433,3 +456,40 @@ option then the minion will log a warning message. - extra_config - minion.d/* - /etc/roles/webserver + + +Frozen Build Update Settings +---------------------------- + +These options control how :py:func:`salt.modules.saltutil.update` works with esky +frozen apps. For more information look at ``_. + +.. conf_minion:: update_url + +``update_url`` +-------------- + +Default: ``False`` (Update feature is disabled) + +The url to use when looking for application updates. Esky depends on directory +listings to search for new versions. A webserver running on your Master is a +good starting point for most setups. + +.. code-block:: yaml + + update_url: 'http://salt.example.com/minion-updates' + +.. conf_minion:: update_restart_services + +``update_restart_services`` +--------------------------- + +Default: ``[]`` (service restarting on update is disabled) + +A list of services to restart when the minion software is updated. This would +typically just be a list containing the minion's service name, but you may +have other services that need to go with it. + +.. code-block:: yaml + + update_restart_services: ['salt-minion'] diff --git a/doc/ref/esky.rst b/doc/ref/esky.rst new file mode 100644 index 0000000000..863e9ed86f --- /dev/null +++ b/doc/ref/esky.rst @@ -0,0 +1,85 @@ +====================================== +Automatic Updates / Frozen Deployments +====================================== + +.. versionadded:: 0.10.3.d + +Salt has support for the +`Esky `_ application freezing and update +tool. This tool allows one to build a complete zipfile out of the salt scripts +and all their dependencies - including shared objects / DLLs. + +Getting Started +=============== + +To build frozen applications, you'll need a suitable build environment for each +of your platforms. You should probably set up a virtualenv in order to limit +the scope of Q/A. + +This process does work on Windows. Follow the directions at +``_ for details on +installing Salt in Windows. Only the 32-bit Python and dependencies have been +tested, but they have been tested on 64-bit Windows. + +You will need to install ``esky`` and ``bbfreeze`` from Pypi in order to enable +the ``bdist_esky`` command in ``setup.py``. + +Building and Freezing +===================== + +Once you have your tools installed and the environment configured, you can then +``python setup.py sdist`` to get the eggs prepared. After that is done, run +``python setup.py bdist_esky`` to have Esky traverse the module tree and pack +all the scripts up into a redistributable. There will be an appropriately +versioned ``salt-VERSION.zip`` in ``dist/`` if everything went smoothly. + +Windows +------- +You will need to add ``C:\Python27\lib\site-packages\zmq`` to your PATH +variable. This helps bbfreeze find the zmq dll so it can pack it up. + +Using the Frozen Build +====================== + +Unpack the zip file in your desired install location. Scripts like +``salt-minion`` and ``salt-call`` will be in the root of the zip file. The +associated libraries and bootstrapping will be in the directories at the same +level. (Check the `Esky `_ documentation +for more information) + +To support updating your minions in the wild, put your builds on a web server +that your minions can reach. :py:func:`salt.modules.saltutil.update` will +trigger an update and (optionally) a restart of the minion service under the +new version. + +Gotchas +======= + +My Windows minion isn't responding +---------------------------------- +The process dispatch on Windows is slower than it is on *nix. You may need to +add '-t 15' to your salt calls to give them plenty of time to return. + +Windows and the Visual Studio Redist +------------------------------------ +You will need to install the Visual C++ 2008 32-bit redistributable on all +Windows minions. Esky has an option to pack the library into the zipfile, +but OpenSSL does not seem to acknowledge the new location. If you get a +``no OPENSSL_Applink`` error on the console when trying to start your +frozen minion, you have forgotten to install the redistributable. + +Mixed Linux environments and Yum +-------------------------------- +The Yum Python module doesn't appear to be available on any of the standard +Python package mirrors. If you need to support RHEL/CentOS systems, you +should build on that platform to support all your Linux nodes. Also remember +to build your virtualenv with ``--system-site-packages`` so that the +``yum`` module is included. + +Automatic (Python) module discovery +----------------------------------- +Automatic (Python) module discovery does not work with the late-loaded scheme that +Salt uses for (Salt) modules. You will need to explicitly add any +misbehaving modules to the ``freezer_includes`` in Salt's ``setup.py``. +Always check the zipped application to make sure that the necessary modules +were included. diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index f2566120f5..0dde85bea2 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -22,8 +22,11 @@ Full list of builtin modules apt archive augeas_cfg + bluez brew butterkvm + ca + cassandra cluster cmdmod cp @@ -34,6 +37,7 @@ Full list of builtin modules disk django ebuild + event file freebsdjail freebsdkmod @@ -52,16 +56,22 @@ Full list of builtin modules launchctl linux_sysctl mdadm + monit moosefs mount mysql network nginx + nzbget + openbsdpkg + openbsdservice osxdesktop pacman pillar pip + pkgng postgres + poudriere ps publish puppet @@ -96,6 +106,7 @@ Full list of builtin modules win_service win_shadow win_useradd - yumpkg yumpkg5 + yumpkg + zfs zypper diff --git a/doc/ref/modules/all/salt.modules.bluez.rst b/doc/ref/modules/all/salt.modules.bluez.rst new file mode 100644 index 0000000000..2a11833110 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.bluez.rst @@ -0,0 +1,6 @@ +================== +salt.modules.bluez +================== + +.. automodule:: salt.modules.bluez + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.ca.rst b/doc/ref/modules/all/salt.modules.ca.rst new file mode 100644 index 0000000000..e0f99cb6d4 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.ca.rst @@ -0,0 +1,6 @@ +=============== +salt.modules.ca +=============== + +.. automodule:: salt.modules.ca + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.cassandra.rst b/doc/ref/modules/all/salt.modules.cassandra.rst new file mode 100644 index 0000000000..823880cdd0 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.cassandra.rst @@ -0,0 +1,6 @@ +====================== +salt.modules.cassandra +====================== + +.. automodule:: salt.modules.cassandra + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.event.rst b/doc/ref/modules/all/salt.modules.event.rst new file mode 100644 index 0000000000..56df526f69 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.event.rst @@ -0,0 +1,6 @@ +================== +salt.modules.event +================== + +.. automodule:: salt.modules.event + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.monit.rst b/doc/ref/modules/all/salt.modules.monit.rst new file mode 100644 index 0000000000..4ec6b5c471 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.monit.rst @@ -0,0 +1,6 @@ +================== +salt.modules.monit +================== + +.. automodule:: salt.modules.monit + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.nzbget.rst b/doc/ref/modules/all/salt.modules.nzbget.rst new file mode 100644 index 0000000000..8d53d1ed8d --- /dev/null +++ b/doc/ref/modules/all/salt.modules.nzbget.rst @@ -0,0 +1,6 @@ +=================== +salt.modules.nzbget +=================== + +.. automodule:: salt.modules.nzbget + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.openbsdpkg.rst b/doc/ref/modules/all/salt.modules.openbsdpkg.rst new file mode 100644 index 0000000000..573664bb4f --- /dev/null +++ b/doc/ref/modules/all/salt.modules.openbsdpkg.rst @@ -0,0 +1,6 @@ +======================= +salt.modules.openbsdpkg +======================= + +.. automodule:: salt.modules.openbsdpkg + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.openbsdservice.rst b/doc/ref/modules/all/salt.modules.openbsdservice.rst new file mode 100644 index 0000000000..ee0f6b51d7 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.openbsdservice.rst @@ -0,0 +1,6 @@ +=========================== +salt.modules.openbsdservice +=========================== + +.. automodule:: salt.modules.openbsdservice + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.pkgng.rst b/doc/ref/modules/all/salt.modules.pkgng.rst new file mode 100644 index 0000000000..0c1357b2a7 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.pkgng.rst @@ -0,0 +1,6 @@ +================== +salt.modules.pkgng +================== + +.. automodule:: salt.modules.pkgng + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.poudriere.rst b/doc/ref/modules/all/salt.modules.poudriere.rst new file mode 100644 index 0000000000..f67be1b9f3 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.poudriere.rst @@ -0,0 +1,6 @@ +====================== +salt.modules.poudriere +====================== + +.. automodule:: salt.modules.poudriere + :members: \ No newline at end of file diff --git a/doc/ref/modules/all/salt.modules.zfs.rst b/doc/ref/modules/all/salt.modules.zfs.rst new file mode 100644 index 0000000000..8a14eedf27 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.zfs.rst @@ -0,0 +1,6 @@ +================ +salt.modules.zfs +================ + +.. automodule:: salt.modules.zfs + :members: \ No newline at end of file diff --git a/doc/ref/modules/index.rst b/doc/ref/modules/index.rst index 4f8e7ef0df..e6d2692343 100644 --- a/doc/ref/modules/index.rst +++ b/doc/ref/modules/index.rst @@ -159,7 +159,7 @@ Documentation ============= Salt modules are self documenting, the :func:`sys.doc` function will return the -documentation for all available Facter modules: +documentation for all available modules: .. code-block:: bash diff --git a/doc/ref/peer.rst b/doc/ref/peer.rst index b3ec8cd412..638e30fbb9 100644 --- a/doc/ref/peer.rst +++ b/doc/ref/peer.rst @@ -6,7 +6,7 @@ Salt 0.9.0 introduced the capability for Salt minions to publish commands. The intent of this feature is not for Salt minions to act as independent brokers one with another, but to allow Salt minions to pass commands to each other. -In Salt 1.0 the ability to execute runners from the master was added. This +In Salt 0.10.0 the ability to execute runners from the master was added. This allows for the master to return collective data from runners back to the minions via the peer interface. diff --git a/doc/ref/pillar/all/index.rst b/doc/ref/pillar/all/index.rst new file mode 100644 index 0000000000..eb0bb91b13 --- /dev/null +++ b/doc/ref/pillar/all/index.rst @@ -0,0 +1,15 @@ +.. _all-salt.pillars: + +============================ +Full list of builtin pillars +============================ + +.. currentmodule:: salt.pillar + +.. autosummary:: + :toctree: + :template: autosummary.rst.tmpl + + cmd_yaml + hiera + mongo diff --git a/doc/ref/pillar/all/salt.pillar.cmd_yaml.rst b/doc/ref/pillar/all/salt.pillar.cmd_yaml.rst new file mode 100644 index 0000000000..05b8abdbd1 --- /dev/null +++ b/doc/ref/pillar/all/salt.pillar.cmd_yaml.rst @@ -0,0 +1,6 @@ +==================== +salt.pillar.cmd_yaml +==================== + +.. automodule:: salt.pillar.cmd_yaml + :members: diff --git a/doc/ref/pillar/all/salt.pillar.hiera.rst b/doc/ref/pillar/all/salt.pillar.hiera.rst new file mode 100644 index 0000000000..cdb4973ae6 --- /dev/null +++ b/doc/ref/pillar/all/salt.pillar.hiera.rst @@ -0,0 +1,6 @@ +================= +salt.pillar.hiera +================= + +.. automodule:: salt.pillar.hiera + :members: diff --git a/doc/ref/pillar/all/salt.pillar.mongo.rst b/doc/ref/pillar/all/salt.pillar.mongo.rst new file mode 100644 index 0000000000..bf3886d4ee --- /dev/null +++ b/doc/ref/pillar/all/salt.pillar.mongo.rst @@ -0,0 +1,6 @@ +================= +salt.pillar.mongo +================= + +.. automodule:: salt.pillar.mongo + :members: diff --git a/doc/ref/pillar/index.rst b/doc/ref/pillar/index.rst new file mode 100644 index 0000000000..1fb7875f07 --- /dev/null +++ b/doc/ref/pillar/index.rst @@ -0,0 +1,15 @@ + +.. _salt-pillars: + +======= +Pillars +======= + +Salt includes a number of built-in external pillars, listed at +:ref:`all-salt.pillars`. + +You may also wish to look at the standard pillar documentation, at +:ref:`pillar-configuration` + +The source for the built-in Salt returners can be found here: +:blob:`salt/pillar` diff --git a/doc/ref/renderers/all/index.rst b/doc/ref/renderers/all/index.rst index 75128d25f0..b18921bb24 100644 --- a/doc/ref/renderers/all/index.rst +++ b/doc/ref/renderers/all/index.rst @@ -13,7 +13,14 @@ Full list of builtin renderers json_jinja json_mako json_wempy +<<<<<<< HEAD yaml_jinja yaml_mako yaml_wempy py +======= + py + yaml_jinja + yaml_mako + yaml_wempy +>>>>>>> 267d900f19a3f62c323a65af414a3b7f65233484 diff --git a/doc/ref/renderers/all/salt.renderers.json_wempy.rst b/doc/ref/renderers/all/salt.renderers.json_wempy.rst new file mode 100644 index 0000000000..e47a0269f0 --- /dev/null +++ b/doc/ref/renderers/all/salt.renderers.json_wempy.rst @@ -0,0 +1,6 @@ +========================= +salt.renderers.json_wempy +========================= + +.. automodule:: salt.renderers.json_wempy + :members: \ No newline at end of file diff --git a/doc/ref/renderers/all/salt.renderers.yaml_wempy.rst b/doc/ref/renderers/all/salt.renderers.yaml_wempy.rst new file mode 100644 index 0000000000..72a85a7941 --- /dev/null +++ b/doc/ref/renderers/all/salt.renderers.yaml_wempy.rst @@ -0,0 +1,6 @@ +========================= +salt.renderers.yaml_wempy +========================= + +.. automodule:: salt.renderers.yaml_wempy + :members: \ No newline at end of file diff --git a/doc/ref/renderers/index.rst b/doc/ref/renderers/index.rst index f48c0a83c8..3f42a1a1bc 100644 --- a/doc/ref/renderers/index.rst +++ b/doc/ref/renderers/index.rst @@ -53,7 +53,8 @@ Writing a renderer is easy, all that is required is that a Python module is placed in the rendered directory and that the module implements the render function. The render function will be passed the path of the SLS file. In the render function, parse the passed file and return the data structure -derived from the file. +derived from the file. You can place your custom renderers in a ``_renderers`` +directory in your file root (``/srv/salt/``). Examples -------- diff --git a/doc/ref/returners/all/salt.returners.mongo_return.rst b/doc/ref/returners/all/salt.returners.mongo_return.rst index bf1e88e8a2..1c07676ea5 100644 --- a/doc/ref/returners/all/salt.returners.mongo_return.rst +++ b/doc/ref/returners/all/salt.returners.mongo_return.rst @@ -3,4 +3,4 @@ salt.returners.mongo_return =========================== .. automodule:: salt.returners.mongo_return - :members: \ No newline at end of file + :members: diff --git a/doc/ref/states/all/index.rst b/doc/ref/states/all/index.rst index a1e59be745..2f482a7a58 100644 --- a/doc/ref/states/all/index.rst +++ b/doc/ref/states/all/index.rst @@ -19,14 +19,17 @@ Full list of builtin states group host kmod + module mount mysql_database mysql_grants mysql_user network pip + pkgng pkg postgres_database + postgres_user rvm selinux service diff --git a/doc/ref/states/all/salt.states.module.rst b/doc/ref/states/all/salt.states.module.rst new file mode 100644 index 0000000000..262c2f9fa8 --- /dev/null +++ b/doc/ref/states/all/salt.states.module.rst @@ -0,0 +1,6 @@ +================== +salt.states.module +================== + +.. automodule:: salt.states.module + :members: \ No newline at end of file diff --git a/doc/ref/states/all/salt.states.pkgng.rst b/doc/ref/states/all/salt.states.pkgng.rst new file mode 100644 index 0000000000..a590f35867 --- /dev/null +++ b/doc/ref/states/all/salt.states.pkgng.rst @@ -0,0 +1,6 @@ +================= +salt.states.pkgng +================= + +.. automodule:: salt.states.pkgng + :members: \ No newline at end of file diff --git a/doc/ref/states/all/salt.states.postgres_user.rst b/doc/ref/states/all/salt.states.postgres_user.rst new file mode 100644 index 0000000000..f625a4cda3 --- /dev/null +++ b/doc/ref/states/all/salt.states.postgres_user.rst @@ -0,0 +1,6 @@ +========================= +salt.states.postgres_user +========================= + +.. automodule:: salt.states.postgres_user + :members: \ No newline at end of file diff --git a/doc/ref/states/backup_mode.rst b/doc/ref/states/backup_mode.rst index f146b9bfe0..bd18725f4a 100644 --- a/doc/ref/states/backup_mode.rst +++ b/doc/ref/states/backup_mode.rst @@ -3,7 +3,7 @@ State File Backups ================== In 0.10.2 a new feature was added for backing up files that are replaced by -the file.managed and file.recuse states. The new feature is called the backup +the file.managed and file.recurse states. The new feature is called the backup mode. Setting the backup mode is easy, but is can be set in a number of places. diff --git a/doc/ref/states/include.rst b/doc/ref/states/include.rst new file mode 100644 index 0000000000..9379f3aacd --- /dev/null +++ b/doc/ref/states/include.rst @@ -0,0 +1,26 @@ +=================== +Include and Exclude +=================== + +Salt sls files can include other sls files and exclude sls files that have been +otherwise included. This allows for an sls file to easily extend or manipulate +other sls files. + +Exclude +======= + +The exclude statement, added in Salt 0.10.3 allows an sls to hard exclude +another sls file or a specific id. The component is excluded after the +high data has been compiled, so nothing should be able to override an +exclude. + +Since the exclude can remove an id or an sls the type of component to +exclude needs to be defined. an exclude statement that verifies that the +running highstate does not contain the `http` sls and the `/etc/vimrc` id +would look like this: + +.. code-block:: yaml + + exclude: + - sls: http + - id: /etc/vimrc diff --git a/doc/ref/states/ordering.rst b/doc/ref/states/ordering.rst index 40da2a8c85..7969861100 100644 --- a/doc/ref/states/ordering.rst +++ b/doc/ref/states/ordering.rst @@ -210,3 +210,18 @@ set the order to ``last``: vim: pkg.installed: - order: last + +Remember that requisite statements override the order option. So the order +option should be applied to the highest component of the requisite chain: + +.. code-block:: yaml + + vim: + pkg.installed: + - order: last + - require: + - file: /etc/vimrc + + /etc/vimrc: + file.managed: + - source: salt://edit/vimrc diff --git a/doc/ref/states/requisites.rst b/doc/ref/states/requisites.rst index a7904c796e..0da32707cb 100644 --- a/doc/ref/states/requisites.rst +++ b/doc/ref/states/requisites.rst @@ -78,4 +78,94 @@ the service is reloaded or restarted. Use --- -# This needs to be filled in +The ``use`` requisite is used to inherit the arguments passed in another +id declaration. This is useful when many files need to have the same defaults. + +The ``use`` statement was developed primarily for the networking states but +can be used on any states in Salt. This made sense for the networking state +because it can define a long list of options that need to be applied to +multiple network interfaces. + +Require In +---------- + +The ``require_in`` requisite is the literal reverse of ``require``. If +a state declaration needs to be required by another state declaration then +require_in can accommodate it, so these two sls files would be the same in +the end: + +Using ``require`` + +.. code-block:: yaml + + httpd: + pkg: + - installed + service: + - running + - require: + - pkg: httpd + +Using ``require_in`` + +.. code-block:: yaml + + httpd: + pkg: + - installed + - require_in: + - service: httpd + service: + - running + +The ``require_in`` statement is particularly useful when assigning a require +in a sperate sls file. For instance it may be common for httpd to require +components used to set up php or mod_python, but the http state does not need +to be aware of the additional components that require it when it is set up: + +http.sls + +.. code-block:: yaml + + httpd: + pkg: + - installed + service: + - running + - require: + - pkg: httpd + +php.sls + +.. code-block:: yaml + + include: + - http + + php: + pkg: + - installed + - require_in: + - service: httpd + +mod_python.sls + +.. code-block:: yaml + + include: + - http + + mod_python: + pkg: + - installed + - require_in: + - service: httpd + +Now the httpd server will only start if php or mod_python are first verified to +be installed. Thus allowing for a requisite to be defined "after the fact". + +Watch In +-------- + +Watch in functions the same was as require in, but applies a watch statement +rather than a require statement to the external state declaration. diff --git a/doc/ref/states/startup.rst b/doc/ref/states/startup.rst new file mode 100644 index 0000000000..a29e0e9c8a --- /dev/null +++ b/doc/ref/states/startup.rst @@ -0,0 +1,44 @@ +============== +Startup States +============== + +Sometimes it may be desired that the salt minion execute a state run when it is +started. This alleviates the need for the master to initiate a state run on a +new minion and can make provisioning much easier. + +As of Salt 0.10.3 the minion config reads options that allow for states to be +executed at startup. The options are `startup_states`, `sls_list` and +`top_file`. + +The `startup_states` option can be passed one of a number of arguments to +define how to execute states. The available options are: + +highstate + Execute ``state.highstate`` + +sls + Read in the ``sls_list`` option and execute the named sls files + +top + Read in the ``top_file`` option and execute states based on that top file + on the Salt Master + +Examples: +--------- + +Execute ``state.highstate`` when starting the minion: + + +.. code-block:: yaml + + startup_states: highstate + +Execute the sls files `edit.vim` and `hyper`: + +.. code-block:: yaml + + startup_states: sls + + sls_list: + - edit.vim + - hyper diff --git a/doc/ref/states/testing.rst b/doc/ref/states/testing.rst new file mode 100644 index 0000000000..9d098e145e --- /dev/null +++ b/doc/ref/states/testing.rst @@ -0,0 +1,33 @@ +============= +State Testing +============= + +Executing a Salt state run can potentially change many aspects of a system and +it may be desirable to first see what a state run is going to change before +applying the run. + +Salt has a test interface to report on exactly what will be changed, this +interface can be invoked on any of the major state run functions: + +.. code-block:: bash + + # salt \* state.highstate test=True + # salt \* state.sls test=True + # salt \* state.single test=True + +The test run is mandated by adding the ``test=True`` option to the states. The +return information will show states that will be applied in yellow and the +result is reported as `None`. + +Default Test +============ + +If the value `test` is set to True in the minion configuration file then states +will default to being executed in test mode. If this value is set then states +can still be run by calling test=False: + +.. code-block:: bash + + # salt \* state.highstate test=False + # salt \* state.sls test=False + # salt \* state.single test=False diff --git a/doc/topics/community.rst b/doc/topics/community.rst index 71521f06e3..b0e4eea247 100644 --- a/doc/topics/community.rst +++ b/doc/topics/community.rst @@ -35,6 +35,12 @@ can use the `Freenode webchat client`__ right from your browser. .. __: http://webchat.freenode.net/?channels=salt&uio=Mj10cnVlJjk9dHJ1ZSYxMD10cnVl83 .. __: http://irclog.perlgeek.de/salt/ +Salt development +---------------- + +If you wish to discuss the development of Salt itself join us in +``#salt-devel``. + .. _community-github: Follow on Github @@ -69,6 +75,8 @@ A few examples of salt states from the community: * https://github.com/kevingranade/kevingranade-salt-state * https://github.com/uggedal/states * https://github.com/mattmcclean/salt-openstack/tree/master/salt +* https://github.com/rentalita/ubuntu-setup/ +* https://github.com/brutasse/states Follow on ohloh =============== diff --git a/doc/topics/event/index.rst b/doc/topics/event/index.rst index c10f0e452a..8b8af5445a 100644 --- a/doc/topics/event/index.rst +++ b/doc/topics/event/index.rst @@ -18,7 +18,7 @@ by the same system user that Salt is running as. To listen to events a SaltEvent object needs to be created and then the get_event function needs to be run. The SaltEvent object needs to know the location that the Salt unix sockets are kept. In the configuration this is the ``sock_dir`` option. The -``sock_dir`` option defaults to "/tmp/.salt-unix" on most systems. +``sock_dir`` option defaults to "/var/run/salt" on most systems. The following code will check for a single event: @@ -26,7 +26,7 @@ The following code will check for a single event: import salt.utils.event - event = salt.utils.event.MasterEvent('/tmp/.salt-unix') + event = salt.utils.event.MasterEvent('/var/run/salt') data = event.get_event() @@ -41,19 +41,19 @@ instead of the default 5. import salt.utils.event - event = salt.utils.event.MasterEvent('/tmp/.salt-unix') + event = salt.utils.event.MasterEvent('/var/run/salt') data = event.get_event(wait=10, tag='auth') -Instead of looking for a single event, the iter_event method can be used to -make a generator which will continually yield salt events. The iter_event +Instead of looking for a single event, the iter_events method can be used to +make a generator which will continually yield salt events. The iter_events method also accepts a tag, but not a wait time: .. code-block:: python import salt.utils.event - event = salt.utils.event.MasterEvent('/tmp/.salt-unix') + event = salt.utils.event.MasterEvent('/var/run/salt') - for data in event.iter_event(tag='auth'): + for data in event.iter_events(tag='auth'): print(data) diff --git a/doc/topics/installation/index.rst b/doc/topics/installation/index.rst index 6f9aba38f2..6cad979e34 100644 --- a/doc/topics/installation/index.rst +++ b/doc/topics/installation/index.rst @@ -50,3 +50,4 @@ Platform-specific installation instructions freebsd gentoo windows + solaris diff --git a/doc/topics/installation/solaris.rst b/doc/topics/installation/solaris.rst new file mode 100644 index 0000000000..93ac2e0268 --- /dev/null +++ b/doc/topics/installation/solaris.rst @@ -0,0 +1,106 @@ +======= +Solaris +======= + +Salt was added to the OpenCSW package repository in September of 2012 by Romeo Theriault at version 0.10.2 of Salt. It has mainly been tested on Solaris 10 (sparc), though it is built for, and should work fine on Solaris 10 (x86), Solaris 9 (sparc/x86) and 11 (sparc/x86) also. Most of the testing has also just focused on the minion, though it has verified that the master starts up successfully on Solaris 10. + +Comments and patches for better support on these platforms is very welcome. Currently at version 0.10.2 of salt, grain detection is weak but patches that very much improve the grain detection will be released in 0.10.3. Work is also underway to include support for services and packages in Solaris. + +Salt is dependent on the following additional packages. These will automatically be installed as +dependencies of the ``py_salt`` package. :: + + py_yaml + py_pyzmq + py_jinja2 + py_msgpack_python + py_m2crypto + py_crypto + python + +Installation +============ + +To install Salt from the OpenCSW package repository you first need to install `pkgutil`_ assuming you don't already have it installed: + +On Solaris 10: + +.. code-block:: bash + + pkgadd -d http://get.opencsw.org/now + +On Solaris 9: + +.. code-block:: bash + + wget http://mirror.opencsw.org/opencsw/pkgutil.pkg + pkgadd -d pkgutil.pkg all + +Once pkgutil is installed you'll need to edit it's config file ``/etc/opt/csw/pkgutil.conf`` to point it at the unstable catalog: + +.. code-block:: diff + + - #mirror=http://mirror.opencsw.org/opencsw/testing + + mirror=http://mirror.opencsw.org/opencsw/unstable + +Ok, time to install salt. + +.. code-block:: bash + + # Update the catalog + root> /opt/csw/bin/pkgutil -U + # Install salt + root> /opt/csw/bin/pkgutil -i -y py_salt + +Minion Configuration +============= + +Now that salt is installed you can find it's configuration files in: + +``/etc/opt/csw/salt/`` + +You'll want to edit the minion config file to set the name of your salt master server: + +.. code-block:: diff + + - #master: salt + + master: your-salt-server + +You can now start the salt minion like so: + +On Solaris 10: + +.. code-block:: bash + + svcadm enable salt-minion + + +On Solaris 9: + +.. code-block:: bash + + /etc/init.d/salt-minion start + +You should now be able to log onto the salt master and check to see if the salt-minion key is awaiting acceptance: + +.. code-block:: bash + + salt-key -l un + +Accept the key: + +.. code-block:: bash + + salt-key -a + +Run a simple test against the minion: + +.. code-block:: bash + + salt '' test.ping + +Troubleshooting +============= + +Logs are in ``/var/log/salt`` + +.. _pkgutil: http://www.opencsw.org/manual/for-administrators/getting-started.html diff --git a/doc/topics/pillar/index.rst b/doc/topics/pillar/index.rst index 46847a71fc..d6760e2215 100644 --- a/doc/topics/pillar/index.rst +++ b/doc/topics/pillar/index.rst @@ -17,8 +17,7 @@ Salt file server the ``pillar_roots`` option in the master config is based on environments mapping to directories. The pillar data is then mapped to minions based on matchers in a top file which is laid out in the same way as the state top file. Salt pillars can use the same matcher types as the -standard top file, and if you are having difficulty matching a specific minion -in your pillar top file, you may want to specify PCRE matching. +standard top file. The configuration for the ``pillar_roots`` in the master config is identical in behavior and function as the ``file_roots`` configuration: @@ -40,23 +39,6 @@ used for States, and has the same structure: base: '*': - packages - 'someminion': - - someminion-specials - -This simple pillar top file declares that information for all minions can be -found in the ``packages.sls`` file [#nokeyvalueintop]_, while -``someminion-specials.sls`` contains overriding or additional information just -for one special minion. - -.. code-block:: yaml - - base: - '.*': - - match: pcre - - packages - '(someminion|anotherminion)': - - match: pcre - - someminion-specials This further example shows how to enable pcre matching in the salt pillar file. The flexibility enabled by pcre matching is particularly useful in salt pillar @@ -75,12 +57,6 @@ files. {% endif %} somekey: globalvalue -``/srv/pillar/someminion-specials.sls`` - -.. code-block:: yaml - - somekey: specialvalue - Now this data can be used from within modules, renderers, State SLS files, and more via the shared pillar `dict`_: @@ -129,6 +105,16 @@ this: .. _`dict`: http://docs.python.org/library/stdtypes.html#mapping-types-dict +Viewing Minion Pillar +===================== + +Once the pillar is set up the data can be viewed on the minion via the +``pillar.data`` module: + +.. code-block:: bash + + # salt '*' pillar.data + Footnotes --------- diff --git a/doc/topics/releases/0.10.2.rst b/doc/topics/releases/0.10.2.rst index 65d26129c7..eb564138c2 100644 --- a/doc/topics/releases/0.10.2.rst +++ b/doc/topics/releases/0.10.2.rst @@ -49,7 +49,7 @@ more powerful, since the minions that will match can be pre determined. Backup Files ------------ -By default all files replaced by the file.managed and file.recuse states we +By default all files replaced by the file.managed and file.recurse states we simply deleted. 0.10.2 adds a new option. By setting the backup option to ``minion`` the files are backed up before they are replaced. @@ -148,7 +148,7 @@ Bluetooth ^^^^^^^^^ Baisc ``bluez`` support for managing and controlling Bluetooth devices. -Supports scanning as well as pairing/unpairing. +Supports scanning as well as pairing/unpairing by Joseph Hall. Test Updates ============ diff --git a/doc/topics/releases/0.10.3.rst b/doc/topics/releases/0.10.3.rst new file mode 100644 index 0000000000..f3213dfdab --- /dev/null +++ b/doc/topics/releases/0.10.3.rst @@ -0,0 +1,180 @@ +========================= +Salt 0.10.3 Release Notes +========================= + +The latest taste of Salt has come, this release has many fixes and feature +additions. Modifications have been made to make ZeroMQ connections more +reliable, the begining of the ACL system is in place, a new command line +parsing system has been added, dynamic module distribution has become more +environment aware, the new `master_finger` option and many more! + +Major Features +============== + +ACL System +---------- + +The new ACL system has been introduced. The ACL system allows for system users +other than root to execute salt commands. Users can be allowed to execute +specific commands in the same way that minions are opened up to the peer +system. + +The configuration value to open up the ACL system is called ``client_acl`` +and is configured like so: + +.. code-block:: yaml + + client_acl: + fred: + - test..* + - pkg.list_pkgs + +Where `fred` is allowed access to functions in the test module and to the +``pkg.list_pkgs`` function. + +Master Finger Option +-------------------- + +The `master_finger` option has been added to improve the security of minion +provisioning. The `master_finger` option allows for the fingerprint of the +master public key to be set in the configuration file to double verify that the +master is valid. This option was added in response to a motivation to pre +authenticate the master when provisioning new minions to help prevent +man in the middle attacks in some situations. + +Salt Key Fingerprint Generation +------------------------------- + +The ability to generate fingerprints of keys used by Salt has been added to +``salt-key``. The new option `finger` accepts the name of the key to generate +and display a fingerprint for. + +.. code-block:: base + + salt-key -F master + +Will display the fingerprints for the master public and private keys. + +Parsing System +-------------- + +Pedro Algavio, aka s0undt3ch, has added a substantial update to the command +line parsing system that makes the help message output much cleaner and easier +to search through. Salt parsers now have `--versions-report` besides usual +`--version` info which you can provide when reporting any issues found. + +Key Generation +-------------- + +We have reduced the requirements needed for `salt-key` to generate minion keys. +You're no longer required to have salt configured and it's common directories +created just to generate keys. This might prove useful if you're batch creating +keys to pre-load on minions. + +Startup States +-------------- + +A few configuration options have been added which allow for states to be run +when the minion daemon starts. This can be a great advantage when deploying +with Salt because the minion can apply states right when it first runs. To +use startup states set the ``startup_states`` configuration option on the +minion to `highstate`. + +New Exclude Declaration +----------------------- + +Some users have asked about adding the ability to ensure that other sls files +or ids are excluded from a state run. The exclude statement will delete all of +the data loaded from the specified sls file or will delete the specified id: + +.. code-block:: yaml + + exclude: + - sls: http + - id: /etc/vimrc + +Max Open Files +-------------- + +While we're currently unable to properly handle ZeroMQ's abort signals when the +max open files is reached, due to the way that's handled on ZeroMQ's, we have +minimized the chances of this happening without at least warning the user. + +More State Output Options +------------------------- + +Some major changes have been made to the state output system. In the past state +return data was printed in a very verbose fashion and only states that failed +or made changes were printed by default. Now two options can be passed to the +master and minion configuration files to change the behavior of the state +output. State output can be set to verbose (default) or non-verbose with the +``state_verbose`` option: + +.. code-block:: yaml + + state_verbose: False + +It is noteworthy that the state_verbose option used to be set to `False` by +default but has been changed to `True` by default in 0.10.3 due to many +requests for the change. + +Te next option to be aware of new and called ``state_output``. This option +allows for the state output to be set to `full` (default) or `terse`. + +The `full` output is the standard state output, but the new `terse` output +will print only one line per state making the output much easier to follow when +executing a large state system. + +.. code-block:: yaml + + state_output: terse + + +`state.file.append` Improvements +-------------------------------- + +The salt state `file.append()` tries *not* to append existing text. Previously +the matching check was being made line by line. While this kind of check might +be enough for most cases, if the text being appended was multi-line, the check +would not work properly. This issue is now properly handled, the match is done +as a whole ignoring any white space addition or removal except inside commas. +For those thinking that, in order to properly match over multiple lines, salt +will load the whole file into memory, that's not true. For most cases this is +not important but an erroneous order to read a 4GB file, if not properly +handled, like salt does, could make salt chew that amount of memory. Salt has +a buffered file reader which will keep in memory a maximum of 256KB and +iterates over the file in chunks of 32KB to test for the match, more than +enough, if not, explain your usage on a ticket. With this change, also +`salt.modules.file.contains()`, `salt.modules.file.contains_regex()`, +`salt.modules.file.contains_glob()` and `salt.utils.find` now do the searching +and/or matching using the buffered chunks approach explained above. + +Two new keyword arguments were also added, `makedirs` and `source`. +The first, `makedirs` will create the necessary directories in order to append +to the specified file, of course, it only applies if we're trying to append to +a non-existing file on a non-existing directory: + +.. code-block:: yaml + + /tmp/salttest/file-append-makedirs: + file.append: + text: foo + makedirs: True + + +The second, `source`, allows to append the contents of a file instead of +specifying the text. + +.. code-block:: yaml + + /tmp/salttest/file-append-source: + + file.append: + - source: salt://testfile + +Security Fix +============ + +A timing vulnerability was uncovered in the code which decrypts the AES +messages sent over the network. This has been fixed and upgrading is +strongly recommended. diff --git a/doc/topics/releases/0.9.3.rst b/doc/topics/releases/0.9.3.rst index 57ecd34a93..c9619ede97 100644 --- a/doc/topics/releases/0.9.3.rst +++ b/doc/topics/releases/0.9.3.rst @@ -196,7 +196,7 @@ file.symlink file.recurse The recurse state function will recursively download a directory on the master file server and place it on the minion. Any change in the files on - the master will be pushed to the minion. The recuse function is very + the master will be pushed to the minion. The recurse function is very powerful and has been tested by pushing out the full Linux kernel source. .. code-block:: yaml diff --git a/doc/topics/targeting/nodegroups.rst b/doc/topics/targeting/nodegroups.rst index f8a5b30d7c..4b20cceb53 100644 --- a/doc/topics/targeting/nodegroups.rst +++ b/doc/topics/targeting/nodegroups.rst @@ -8,6 +8,11 @@ Node groups A predefined group of minions declared in the master configuration file :conf_master:`nodegroups` setting as a compound target. +Nodegroups are declared using a compound target specification. The compount +target documentation can be found here: + +:doc:`Compound Matchers ` + For example, in the master config file :conf_master:`nodegroups` setting:: nodegroups: diff --git a/doc/topics/tests/index.rst b/doc/topics/tests/index.rst new file mode 100644 index 0000000000..7c9dabab32 --- /dev/null +++ b/doc/topics/tests/index.rst @@ -0,0 +1,30 @@ +============= +Writing Tests +============= + +Salt uses a test platform to verify functionality of components in a simple +way. Two testing systems exist to enable testing salt functions in somewhat +real environments. The two subsystems available are integration tests and +unit tests. + +Salt uses the python standard library unittest2 system for testing. + +Integration Tests +================= + +The integration tests start up a number of salt daemons to test functionality +in a live environment. These daemons include 2 salt masters, 1 syndic and 2 +minions. This allows for the syndic interface to be tested and master/minion +communication to be verified. All of the integration tests are executed as +live salt commands sent through the started daemons. + +* :doc:`Writing integration tests ` + +Integration tests are particularly good at testing modules, states and shell +commands. + +Unit Tests +========== + +Direct unit tests are also available, these tests are good for internal +functions. diff --git a/doc/topics/tests/integration.rst b/doc/topics/tests/integration.rst new file mode 100644 index 0000000000..36c75fa7cd --- /dev/null +++ b/doc/topics/tests/integration.rst @@ -0,0 +1,214 @@ +================= +Integration Tests +================= + +The Salt integration tests come with a number of classes and methods which +allow for components to be easily tested. These classes are generally inherited +from and provide specific methods for hooking into the running integration test +environment created by the integration tests. + +It is noteworthy that since integration tests validate against a running +environment that they are generally the preferred means to write tests. + +The integration system is all located under tests/integration in the Salt +source tree. + +Integration Classes +=================== + +The integration classes are located in tests/integration/__init__.py and +can be extended therein. There are three classes available to extend: + +ModuleCase +---------- + +Used to define executions run via the master to minions and to call +single modules and states. + +The available methods are as follows: + +run_function: + Run a single salt function and condition the return down to match the + behavior of the raw function call. This will run the command and only + return the results from a single minion to verify. + +state_result: + Return the result data from a single state return + +run_state: + Run the state.single command and return the state return structure + + + + +SyndicCase +---------- + +Used to execute remote commands via a syndic, only used to verify the +capabilities of the Syndic. + +The available methods are as follows: + +run_function: + Run a single salt function and condition the return down to match the + behavior of the raw function call. This will run the command and only + return the results from a single minion to verify. + +ShellCase +--------- + +Shell out to the scripts which ship with Salt. + +The available methods are as follows: + +run_script: + Execute a salt script with the given argument string + +run_salt: + Execute the salt command, pass in the argument string as it would be + passed on the command line. + +run_run: + Execute the salt-run command, pass in the argument string as it would be + passed on the command line. + +run_run_plus: + Execute Salt run and the salt run function and return the data from + each in a dict + +run_key: + Execute the salt-key command, pass in the argument string as it would be + passed on the command line. + +run_cp: + Execute salt-cp, pass in the argument string as it would be + passed on the command line. + +run_call: + Execute salt-call, pass in the argument string as it would be + passed on the command line. + + +Examples +======== + +Module Example via ModuleCase Class +----------------------------------- + +Import the integration module, this module is already added to the python path +by the test execution. Inherit from the ``integration.ModuleCase`` class. The +tests that execute against salt modules should be placed in the +`tests/integration/modules` directory so that they will be detected by the test +system. + +Now the workhorse method ``run_function`` can be used to test a module: + +.. code-block:: python + + + import os + import integration + + + class TestModuleTest(integration.ModuleCase): + ''' + Validate the test module + ''' + def test_ping(self): + ''' + test.ping + ''' + self.assertTrue(self.run_function('test.ping')) + + def test_echo(self): + ''' + test.echo + ''' + self.assertEqual(self.run_function('test.echo', ['text']), 'text') + +ModuleCase can also be used to test states, when testing states place the test +module in the `tests/integration/states` directory. The ``state_result`` and +the ``run_state`` methods are the workhorse here: + +.. code-block:: python + + import os + import shutil + import integration + + HFILE = os.path.join(integration.TMP, 'hosts') + + class HostTest(integration.ModuleCase): + ''' + Validate the host state + ''' + + def setUp(self): + shutil.copyfile(os.path.join(integration.FILES, 'hosts'), HFILE) + super(HostTest, self).setUp() + + def tearDown(self): + if os.path.exists(HFILE): + os.remove(HFILE) + super(HostTest, self).tearDown() + + def test_present(self): + ''' + host.present + ''' + name = 'spam.bacon' + ip = '10.10.10.10' + ret = self.run_state('host.present', name=name, ip=ip) + result = self.state_result(ret) + self.assertTrue(result) + with open(HFILE) as fp_: + output = fp_.read() + self.assertIn('{0}\t\t{1}'.format(ip, name), output) + +The above example also demonstrates using the integration files and the +integration state tree. The variable `integration.FILES` will point to the +directory used to store files that can be used or added to to help enable tests +that require files. The location `integration.TMP` can also be used to store +temporary files that the test system will clean up when the execution finishes. + +The integration state tree can be found at `tests/integration/files/file/base`. +This is where the referenced `host.present` sls file resides. + +Shell Example via ShellCase +--------------------------- + +Validating the shell commands can be done via shell tests. Here are some +examples: + +.. code-block:: python + + import sys + import shutil + import tempfile + + import integration + + class KeyTest(integration.ShellCase): + ''' + Test salt-key script + ''' + + _call_binary_ = 'salt-key' + + def test_list(self): + ''' + test salt-key -L + ''' + data = self.run_key('-L') + expect = [ + 'Unaccepted Keys:', + 'Accepted Keys:', + 'minion', + 'sub_minion', + 'Rejected:', ''] + self.assertEqual(data, expect) + +This example verifies that the ``salt-key`` command executes and returns as +expected by making use of the ``run_key`` method. + +All shell tests should be placed in the `tests/integraion/shell` directory. diff --git a/doc/topics/troubleshooting/index.rst b/doc/topics/troubleshooting/index.rst index 928eb836bb..fc645fa716 100644 --- a/doc/topics/troubleshooting/index.rst +++ b/doc/topics/troubleshooting/index.rst @@ -182,3 +182,26 @@ Common YAML Gotchas An extensive list of :doc:`YAML idiosyncrasies` has been compiled. + +Live Python Debug Output +======================== + +If the minion or master seems to be unresponsive, a SIGUSR1 can be passed to +the processes to display where in the code they are running. If encountering a +situation like this, this debug information can be invaluable. First make +sure the master of minion are running in the foreground: + +.. code-block:: bash + + # salt-master -l debug + # salt-minion -l debug + +The pass the signal to the master or minion when it seems to be unresponsive: + +.. code-block:: bash + + killall -SIGUSR1 salt-master + killall -SIGUSR1 salt-minion + +When filing an issue or sending questions to the mailing list for a problem +with an unresponsive daemon this information can be invaluable. diff --git a/doc/topics/troubleshooting/yaml_idiosyncrasies.rst b/doc/topics/troubleshooting/yaml_idiosyncrasies.rst index 0e7bb49516..7be4afb7ae 100644 --- a/doc/topics/troubleshooting/yaml_idiosyncrasies.rst +++ b/doc/topics/troubleshooting/yaml_idiosyncrasies.rst @@ -78,7 +78,7 @@ is not desirable, then a deeply nested dict can be declared with curly braces: Integers are Parsed as Integers =============================== -NOTE: This has been fixed in salt 0.9.10, as of this release passing an +NOTE: This has been fixed in salt 0.10.0, as of this release passing an integer that is preceded by a 0 will be correctly parsed When passing `integers`_ into an SLS file, they are passed as integers. This means @@ -99,7 +99,7 @@ This is best explained when setting the mode for a file: Salt manages this well, since the mode is passed as 644, but if the mode is zero padded as 0644, then it is read by YAML as an integer and evaluated as -a hexadecimal value, 0644 becomes 420. Therefore, if the file mode is +an octal value, 0644 becomes 420. Therefore, if the file mode is preceded by a 0 then it needs to be passed as a string: .. code-block:: yaml @@ -173,3 +173,39 @@ WORKS: - name: AAAAB3NzaC... - enc: dsa +YAML support only plain ASCII +============================= + +According to YAML specification, only ASCII characters can be used. + +Within double-quotes, special characters may be represented with C-style +escape sequences starting with a backslash ( \\ ). + +Examples: + +.. code-block:: yaml + + - micro: "\u00b5" + - copyright: "\u00A9" + - A: "\x41" + - alpha: "\u0251" + - Alef: "\u05d0" + + + +List of useable `Unicode characters`_ will help you to identify correct numbers. + +.. _`Unicode characters`: http://en.wikipedia.org/wiki/List_of_Unicode_characters + + +Python can also be used to discover the Unicode number for a character: + +.. code-block:: python + + repr(u"Text with wrong characters i need to figure out") + +This shell command can find wrong characters in your SLS files: + +.. code-block: shell + find . -name '*.sls' -exec grep --color='auto' -P -n '[^\x00-\x7F]' \{} \; + diff --git a/doc/topics/tutorials/extend_with_require_watch.rst b/doc/topics/tutorials/extend_with_require_watch.rst new file mode 100644 index 0000000000..3f22ebada9 --- /dev/null +++ b/doc/topics/tutorials/extend_with_require_watch.rst @@ -0,0 +1,4 @@ +.. admonition:: Using extend with require or watch + + The ``extend`` statement works differently for ``require`` or ``watch``. + It appends to, rather than replacing the requisite component. diff --git a/doc/topics/tutorials/requisite_incl.rst b/doc/topics/tutorials/requisite_incl.rst index 6801fdc25f..db8de9d995 100644 --- a/doc/topics/tutorials/requisite_incl.rst +++ b/doc/topics/tutorials/requisite_incl.rst @@ -4,6 +4,7 @@ following the :doc:`installation ` and the :doc:`con .. admonition:: Stuck? - If you get stuck at any point, there are many ways to :doc:`get help from - the Salt community ` including our mailing list and our - IRC channel. + There are many ways to :doc:`get help from the Salt community + ` including our + `mailing list `_ + and our `IRC channel `_ #salt. diff --git a/doc/topics/tutorials/starting_states.rst b/doc/topics/tutorials/starting_states.rst index 16e193c85c..657506d5ee 100644 --- a/doc/topics/tutorials/starting_states.rst +++ b/doc/topics/tutorials/starting_states.rst @@ -145,8 +145,8 @@ out as just files. A SLS is just a file and files to download are just files. The Apache example would be laid out in the root of the Salt file server like this: :: - /apache/init.sls - /apache/httpd.conf + apache/init.sls + apache/httpd.conf So the httpd.conf is just a file in the apache directory, and is referenced directly. @@ -154,7 +154,7 @@ directly. But with more than a single SLS file, more components can be added to the toolkit, consider this SSH example: -``/ssh/init.sls:`` +``ssh/init.sls:`` .. code-block:: yaml :linenos: @@ -211,13 +211,13 @@ toolkit, consider this SSH example: Now our State Tree looks like this: :: - /apache/init.sls - /apache/httpd.conf - /ssh/init.sls - /ssh/server.sls - /ssh/banner - /ssh/ssh_config - /ssh/sshd_config + apache/init.sls + apache/httpd.conf + ssh/init.sls + ssh/server.sls + ssh/banner + ssh/ssh_config + ssh/sshd_config This example now introduces the ``include`` statement. The include statement includes another SLS file so that components found in it can be required, @@ -236,7 +236,7 @@ needs to be placed. These examples will add more watchers to apache and change the ssh banner. -``/ssh/custom-server.sls:`` +``ssh/custom-server.sls:`` .. code-block:: yaml :linenos: @@ -249,7 +249,7 @@ These examples will add more watchers to apache and change the ssh banner. file: - source: salt://ssh/custom-banner -``/python/mod_python.sls:`` +``python/mod_python.sls:`` .. code-block:: yaml :linenos: @@ -273,9 +273,7 @@ to configure the banner. In the new mod_python SLS the mod_python package is added, but more importantly the apache service was extended to also watch the mod_python package. -There is a bit of a trick here, in the extend statement Requisite Statements -are extended, so the ``- pkg: mod_python`` is appended to the watch list. But -all other statements are overwritten. +.. include:: extend_with_require_watch.rst Understanding the Render System =============================== @@ -313,7 +311,7 @@ available, ``salt``, ``grains``, and ``pillar``. The ``salt`` object allows for any Salt function to be called from within the template, and ``grains`` allows for the Grains to be accessed from within the template. A few examples: -``/apache/init.sls:`` +``apache/init.sls:`` .. code-block:: yaml :linenos: @@ -356,7 +354,7 @@ Red Hat, then the name of the Apache package and service needs to be httpd. A more aggressive way to use Jinja can be found here, in a module to set up a MooseFS distributed filesystem chunkserver: -``/moosefs/chunk.sls:`` +``moosefs/chunk.sls:`` .. code-block:: yaml :linenos: @@ -428,7 +426,7 @@ but a SLS file set to use another renderer can be easily added to the tree. This example shows a very basic Python SLS file: -``/python/django.sls:`` +``python/django.sls:`` .. code-block:: python :linenos: @@ -467,14 +465,14 @@ needed by using a pure Python SLS. Running and debugging salt states. ---------------------------------- -after writing out your top.sls file, to run it you call -``salt '*' state.highstate``. If you get back just the hostnames with -a : after, but no return, then chances are there is a problem with the sls -files. To debug these, to see what's going on, and see the errors, use the -``salt-call`` command like so: ``salt-call state.highstate -l debug``. This -should help you figure out what's going wrong. You can also start the minions -in the foreground in debug mode, as a possible way to help debug as well. -To start the minion in debug mode call it like this: ``salt-minion -l debug``. +Once the rules in an SLS are ready, they need to be tested to ensure they +work properly. To invoke the rules, simply execute ``salt '*' state.highstate`` +on the command line. If you get back just the hostnames with a `:` after, +but no return, chances are there is a problem with the one or more of the sls +files. Use the ``salt-call`` command: ``salt-call state.highstate -l debug`` +and examine the output for errors. This should help troubleshoot the issue. +The minions can also be started in the foreground in debug mode. Start the +minion in debug mode with: ``salt-minion -l debug``. Now onto the :doc:`States tutorial, part 1`. diff --git a/doc/topics/tutorials/states_pt2.rst b/doc/topics/tutorials/states_pt2.rst index 36fe80988f..fe9a2a16e7 100644 --- a/doc/topics/tutorials/states_pt2.rst +++ b/doc/topics/tutorials/states_pt2.rst @@ -2,12 +2,14 @@ States tutorial, part 2 ======================= -This tutorial builds on the topic covered in :doc:`part 1 `. It is -recommended that you begin there. +.. note:: -In the last Salt States tutorial we covered the basics of installing a package. -In this tutorial we will modify our ``webserver.sls`` file to be more -complicated, have requirements, and use even more Salt States. + This tutorial builds on the topic covered in :doc:`part 1 `. + It is recommended that you begin there. + +In the :doc:`last part ` of the Salt States tutorial we covered +the basics of installing a package. We will now modify our ``webserver.sls`` +file to have requirements, and use even more Salt States. Call multiple States ==================== @@ -158,4 +160,4 @@ Next steps ========== In :doc:`part 3 ` we will discuss how to use includes, extends and -templating to make hugely complicated State Tree configurations dead-simple. +templating to make a more complete State Tree configuration. diff --git a/doc/topics/tutorials/states_pt3.rst b/doc/topics/tutorials/states_pt3.rst index 7aab658477..7a70c98e8c 100644 --- a/doc/topics/tutorials/states_pt3.rst +++ b/doc/topics/tutorials/states_pt3.rst @@ -2,26 +2,27 @@ States tutorial, part 3 ======================= -This tutorial builds on the topic covered in :doc:`part 2 `. It is -recommended that you begin there. +.. note:: -This tutorial will cover more advanced templating and configuration techniques -for ``sls`` files. + This tutorial builds on the topic covered in :doc:`part1 ` and + :doc:`part 2 `. It is recommended that you begin there. + +This part of the tutorial will cover more advanced templating and +configuration techniques for ``sls`` files. Templating SLS modules ====================== -SLS modules may require programming logic or inline executions. This is +SLS modules may require programming logic or inline execution. This is accomplished with module templating. The default module templating system used is `Jinja2`_ and may be configured by changing the :conf_master:`renderer` value in the master config. .. _`Jinja2`: http://jinja.pocoo.org/ -All states are passed through a templating system when they are initially read, -so all that is required to make use of the templating system is to add some -templating code. An example of an sls module with templating may look like -this: +All states are passed through a templating system when they are initially read. +To make use of the templating system, simple add some templating markup. +An example of an sls module with templating markup may look like this: .. code-block:: yaml @@ -45,8 +46,8 @@ Using Grains in SLS modules =========================== Often times a state will need to behave differently on different systems. -:doc:`Salt grains ` can be used from within sls modules. An object -called ``grains`` is made available in the template context: +:doc:`Salt grains ` objects are made available +in the template context. The `grains` can be used from within sls modules: .. code-block:: yaml @@ -83,31 +84,31 @@ The Salt module functions are also made available in the template context as {% endfor %} Below is an example that uses the ``network.hwaddr`` function to retrieve the -MAC address for eth0: +MAC address for eth0:: salt['network.hwaddr']('eth0') Advanced SLS module syntax ========================== -Last we will cover some incredibly useful techniques for more complex State +Lastly, we will cover some incredibly useful techniques for more complex State trees. :term:`Include declaration` --------------------------- -You have seen an example of how to spread a Salt tree across several files but -in order to be able to have :term:`requisite references ` -span multiple files you must use an :term:`include declaration`. For example: +A previous example showed how to spread a Salt tree across several files. +Similarly, :term:`requisite references ` span multiple +files by using an :term:`include declaration`. For example: -``python-libs.sls``: +``python/python-libs.sls``: .. code-block:: yaml python-dateutil: pkg.installed -``django.sls``: +``python/django.sls``: .. code-block:: yaml @@ -126,14 +127,14 @@ You can modify previous declarations by using an :term:`extend declaration`. For example the following modifies the Apache tree to also restart Apache when the vhosts file is changed: -``apache.sls``: +``apache/apache.sls``: .. code-block:: yaml apache: pkg.installed -``mywebsite.sls``: +``apache/mywebsite.sls``: .. code-block:: yaml @@ -148,8 +149,9 @@ vhosts file is changed: /etc/httpd/extra/httpd-vhosts.conf: file.managed: - - source: salt://httpd-vhosts.conf + - source: salt://apache/httpd-vhosts.conf +.. include:: extend_with_require_watch.rst :term:`Name declaration` ------------------------ @@ -158,10 +160,10 @@ You can override the :term:`ID declaration` by using a :term:`name declaration`. For example, the previous example is a bit more maintainable if rewritten as follows: -``mywebsite.sls``: +``apache/mywebsite.sls``: .. code-block:: yaml - :emphasize-lines: 8,10,13 + :emphasize-lines: 8,10,12 include: - apache @@ -175,7 +177,7 @@ rewritten as follows: mywebsite: file.managed: - name: /etc/httpd/extra/httpd-vhosts.conf - - source: salt://httpd-vhosts.conf + - source: salt://apache/httpd-vhosts.conf :term:`Names declaration` ------------------------- diff --git a/pkg/rpm/0001-Only-expect-args-if-they-are-actually-passed-in.patch b/pkg/rpm/0001-Only-expect-args-if-they-are-actually-passed-in.patch new file mode 100644 index 0000000000..994b2108b1 --- /dev/null +++ b/pkg/rpm/0001-Only-expect-args-if-they-are-actually-passed-in.patch @@ -0,0 +1,26 @@ +From 4d65fbe3ef36e74c41d96f3c33aa3cf35ab4b09b Mon Sep 17 00:00:00 2001 +From: Joseph Hall +Date: Tue, 31 Jul 2012 05:34:27 -0600 +Subject: [PATCH 12/13] Only expect args if they are actually passed in + +--- + salt/modules/disk.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/salt/modules/disk.py b/salt/modules/disk.py +index 0fca708..0964962 100644 +--- a/salt/modules/disk.py ++++ b/salt/modules/disk.py +@@ -33,7 +33,8 @@ def usage(args=None): + cmd = 'df -kP' + else: + cmd = 'df' +- cmd = cmd + ' -' + args ++ if args: ++ cmd = cmd + ' -' + args + ret = {} + out = __salt__['cmd.run'](cmd).split('\n') + for line in out: +-- +1.7.11.2 + diff --git a/pkg/rpm/salt.spec b/pkg/rpm/salt.spec index 0be95143dd..ee5a96fad8 100644 --- a/pkg/rpm/salt.spec +++ b/pkg/rpm/salt.spec @@ -9,8 +9,8 @@ %{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} Name: salt -Version: 0.10.1 -Release: 1%{?dist} +Version: 0.10.2 +Release: 2%{?dist} Summary: A parallel remote execution system Group: System Environment/Daemons @@ -24,6 +24,7 @@ Source4: %{name}-master.service Source5: %{name}-syndic.service Source6: %{name}-minion.service Source7: README.fedora +Patch0: 0001-Only-expect-args-if-they-are-actually-passed-in.patch BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildArch: noarch @@ -45,7 +46,6 @@ Requires: python26-zmq Requires: python26-jinja2 Requires: python26-PyYAML Requires: python26-m2crypto -Requires: python26-PyXML Requires: python26-msgpack %else @@ -62,7 +62,6 @@ Requires: python-zmq Requires: python-jinja2 Requires: PyYAML Requires: m2crypto -Requires: PyXML Requires: python-msgpack %endif @@ -108,6 +107,7 @@ Salt minion is queried and controlled from the master. %prep %setup -q +%patch0 -p1 %build @@ -261,8 +261,15 @@ fi %endif %changelog -* Sat Jun 16 2012 Clint Savage - 0.10.0-1 -- Moved to upstream release 0.10.0 +* Thu Aug 2 2012 Clint Savage - 0.10.2-2 +- Fix upstream bug #1730 per RHBZ#845295 + +* Sat Jul 31 2012 Clint Savage - 0.10.2-1 +- Moved to upstream release 0.10.2 +- Removed PyXML as a dependency + +* Sat Jun 16 2012 Clint Savage - 0.10.1-1 +- Moved to upstream release 0.10.1 * Sat Apr 28 2012 Clint Savage - 0.9.9.1-1 - Moved to upstream release 0.9.9.1 diff --git a/pkg/salt_bash_completion b/pkg/salt_bash_completion new file mode 100644 index 0000000000..8437f968df --- /dev/null +++ b/pkg/salt_bash_completion @@ -0,0 +1,323 @@ +#!/bin/bash + +# written by David Pravec +# - feel free to /msg alekibango on IRC if you want to talk about this file + +# TODO: check if --config|-c was used and use configured config file for queries +# TODO: solve somehow completion for salt -G pythonversion:[tab] +# (not sure what to do with lists) +# TODO: --range[tab] -- how? +# TODO: -E --exsel[tab] -- how? +# TODO: --compound[tab] -- how? +# TODO: use history to extract some words, esp. if ${cur} is empty +# TODO: TEST EVERYTING a lot +# TODO: cache results of some functions? where? how long? +# TODO: is it ok to use '--timeout 2' ? + + +_salt_get_grains(){ + if [ "$1" = 'local' ] ; then + salt-call --text-out -- grains.ls | sed 's/^.*\[//' | tr -d ",']" |sed 's:\([a-z0-9]\) :\1\: :g' + else + salt '*' --timeout 2 --text-out -- grains.ls | sed 's/^.*\[//' | tr -d ",']" |sed 's:\([a-z0-9]\) :\1\: :g' + fi +} + +_salt_get_grain_values(){ + if [ "$1" = 'local' ] ; then + salt-call --text-out -- grains.item $1 |sed 's/^\S*:\s//' |grep -v '^\s*$' + else + salt '*' --timeout 2 --text-out -- grains.item $1 |sed 's/^\S*:\s//' |grep -v '^\s*$' + fi +} + + +_salt(){ + local cur prev opts _salt_grains _salt_coms pprev ppprev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + if [ ${COMP_CWORD} -gt 2 ]; then + pprev="${COMP_WORDS[COMP_CWORD-2]}" + fi + if [ ${COMP_CWORD} -gt 3 ]; then + ppprev="${COMP_WORDS[COMP_CWORD-3]}" + fi + + opts="--help -h --version -c --compound --raw-out --text-out --json-out --no-color \ + --timeout -t --static -s --batch-size -b -E --pcre -L --list \ + -G --grain --grain-pcre -X --exsel -N --nodegroup -R --range --return \ + -Q --query -c --config -s --static -t --timeout \ + -b --batch-size -X --exsel" + + if [[ "${cur}" == -* ]] ; then + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi + + # 2 special cases for filling up grain values + case "${pprev}" in + -G|--grain|--grain-pcre) + if [ "${cur}" = ":" ]; then + COMPREPLY=($(compgen -W "`_salt_get_grain_values ${prev}`" )) + return 0 + fi + ;; + esac + case "${ppprev}" in + -G|--grain|--grain-pcre) + if [ "${prev}" = ":" ]; then + COMPREPLY=( $(compgen -W "`_salt_get_grain_values ${pprev}`" -- ${cur}) ) + return 0 + fi + ;; + esac + + if [ "${cur}" = "=" ] && [[ "${prev}" == --* ]]; then + cur="" + fi + if [ "${prev}" = "=" ] && [[ "${pprev}" == --* ]]; then + prev="${pprev}" + fi + + case "${prev}" in + + -c|--config) + COMPREPLY=($(compgen -f -- ${cur})) + return 0 + ;; + salt) + COMPREPLY=($(compgen -W "\'*\' ${opts} `salt-key --no-color -l acc`" -- ${cur})) + return 0 + ;; + -E|--pcre) + COMPREPLY=($(compgen -W "`salt-key --no-color -l acc`" -- ${cur})) + return 0 + ;; + -G|--grain|--grain-pcre) + COMPREPLY=($(compgen -W "$(_salt_get_grains)" -- ${cur})) + return 0 + ;; + -C|--compound) + COMPREPLY=() # TODO: finish this one? how? + return 0 + ;; + -t|--timeout) + COMPREPLY=($( compgen -W "1 2 3 4 5 6 7 8 9 10 15 20 30 40 60 90 120 180" -- ${cur})) + return 0 + ;; + -b|--batch|--batch-size) + COMPREPLY=($(compgen -W "1 2 3 4 5 6 7 8 9 10 15 20 30 40 50 60 70 80 90 100 120 150 200")) + return 0 + ;; + -X|--exsel) # TODO: finish this one? how? + return 0 + ;; + -N|--nodegroup) + MASTER_CONFIG='/etc/salt/master' + COMPREPLY=($(compgen -W "`awk -F ':' 'BEGIN {print_line = 0}; /^nodegroups/ {print_line = 1;getline } print_line && /^ */ {print $1} /^[^ ]/ {print_line = 0}' <${MASTER_CONFIG}`" -- ${cur})) + return 0 + ;; + esac + + _salt_coms="$(salt '*' --timeout 2 --text-out -- sys.list_functions | sed 's/^.*\[//' | tr -d ",']" )" + all="${opts} ${_salt_coms}" + COMPREPLY=( $(compgen -W "${all}" -- ${cur}) ) + + return 0 +} + +complete -F _salt salt + + +_saltkey(){ + local cur prev opts prev pprev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="--version -h --help -l --list -L --list-all -a --accept \ + -R --reject-all -p --print -P --print-all -r --reject \ + -d --delete -q --quiet -D --delete-all --key-logfile -c \ + --config -q --quiet --gen-keys --gen-keys-dir \ + --keysize accept-all -A " + if [ ${COMP_CWORD} -gt 2 ]; then + pprev="${COMP_WORDS[COMP_CWORD-2]}" + fi + if [ ${COMP_CWORD} -gt 3 ]; then + ppprev="${COMP_WORDS[COMP_CWORD-3]}" + fi + if [[ "${cur}" == -* ]] ; then + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi + + if [ "${cur}" = "=" ] && [[ "${prev}" == --* ]]; then + cur="" + fi + if [ "${prev}" = "=" ] && [[ "${pprev}" == --* ]]; then + prev="${pprev}" + fi + + case "${prev}" in + -a|--accept) + COMPREPLY=($(compgen -W "$(salt-key -l un --no-color; salt-key -l rej --no-color)" -- ${cur})) + return 0 + ;; + -r|--reject) + COMPREPLY=($(compgen -W "$(salt-key -l acc --no-color)" -- ${cur})) + return 0 + ;; + -d|--delete) + COMPREPLY=($(compgen -W "$(salt-key -l acc --no-color; salt-key -l un --no-color; salt-key -l rej --no-color)" -- ${cur})) + return 0 + ;; + -c|--config) + COMPREPLY=($(compgen -f -- ${cur})) + return 0 + ;; + --keysize) + COMPREPLY=($(compgen -W "2048 3072 4096 5120 6144" -- ${cur})) + return 0 + ;; + --gen-keys) + return 0 + ;; + --gen-keys-dir) + COMPREPLY=($(compgen -d -- ${cur})) + return 0 + ;; + -p|--print) + COMPREPLY=($(compgen -W "$(salt-key -l acc --no-color; salt-key -l un --no-color; salt-key -l rej --no-color)" -- ${cur})) + return 0 + ;; + -l|--list) + COMPREPLY=($(compgen -W "pre un acc accepted unaccepted rej rejected all" -- ${cur})) + return 0 + ;; + --accept-all) + return 0 + ;; + esac + COMPREPLY=($(compgen -W "${opts} " -- ${cur})) + return 0 +} + +complete -F _saltkey salt-key + +_saltcall(){ + local cur prev opts _salt_coms pprev ppprev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -l --log-level -d --doc -m --module-dirs --raw-out --text-out --yaml-out --json-out --no-color" + if [ ${COMP_CWORD} -gt 2 ]; then + pprev="${COMP_WORDS[COMP_CWORD-2]}" + fi + if [ ${COMP_CWORD} -gt 3 ]; then + ppprev="${COMP_WORDS[COMP_CWORD-3]}" + fi + if [[ "${cur}" == -* ]] ; then + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi + + if [ "${cur}" = "=" ] && [[ ${prev} == --* ]]; then + cur="" + fi + if [ "${prev}" = "=" ] && [[ ${pprev} == --* ]]; then + prev="${pprev}" + fi + + case ${prev} in + -m|--module-dirs) + COMPREPLY=( $(compgen -d ${cur} )) + return 0 + ;; + -l|--log-level) + COMPREPLY=( $(compgen -W "info none garbage trace warning error debug" -- ${cur})) + return 0 + ;; + -g|grains) + return 0 + ;; + salt-call) + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + ;; + esac + + _salt_coms="$(salt-call --text-out -- sys.list_functions|sed 's/^.*\[//' | tr -d ",']" )" + COMPREPLY=( $(compgen -W "${opts} ${_salt_coms}" -- ${cur} )) + return 0 +} + +complete -F _saltcall salt-call + + +_saltcp(){ + local cur prev opts target prefpart postpart helper filt pprev ppprev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="-h --help -L --list -E --pcre -G --grain --grain-pcre -R --range -C --compound -c --config= -t --timeout= " + if [[ "${cur}" == -* ]] ; then + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi + + if [ "${cur}" = "=" ] && [[ "${prev}" == --* ]]; then + cur="" + fi + if [ "${prev}" = "=" ] && [[ "${pprev}" == --* ]]; then + prev=${pprev} + fi + + case ${prev} in + salt-cp) + COMPREPLY=($(compgen -W "${opts} `salt-key -l acc --no-color`" -- ${cur})) + return 0 + ;; + -t|--timeout) + # those numbers are just a hint + COMPREPLY=($(compgen -W "2 3 4 8 10 15 20 25 30 40 60 90 120 180 240 300" -- ${cur} )) + return 0 + ;; + -E|--pcre) + COMPREPLY=($(compgen -W "`salt-key -l acc --no-color`" -- ${cur})) + return 0 + ;; + -L|--list) + # IMPROVEMENTS ARE WELCOME + prefpart="${cur%,*}," + postpart=${cur##*,} + filt="^\($(echo ${cur}| sed 's:,:\\|:g')\)$" + helper=($(salt-key -l acc --no-color | grep -v "${filt}" | sed "s/^/${prefpart}/")) + COMPREPLY=($(compgen -W "${helper[*]}" -- ${cur})) + + return 0 + ;; + -G|--grain|--grain-pcre) + COMPREPLY=($(compgen -W "$(_salt_get_grains)" -- ${cur})) + return 0 + ;; + # FIXME + -R|--range) + # FIXME ?? + return 0 + ;; + -C|--compound) + # FIXME ?? + return 0 + ;; + -c|--config) + COMPREPLY=($(compgen -f -- ${cur})) + return 0 + ;; + esac + + # default is using opts: + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) +} + +complete -F _saltcp salt-cp + diff --git a/salt/__init__.py b/salt/__init__.py index 3090842375..4b412a720d 100644 --- a/salt/__init__.py +++ b/salt/__init__.py @@ -1,378 +1,165 @@ ''' Make me some salt! ''' -from salt.version import __version__ # Import python libs import os import sys +import logging +import getpass -# Import salt libs, the try block bypasses an issue at build time so that c +# Import salt libs, the try block bypasses an issue at build time so that # modules don't cause the build to fail +from salt.version import __version__ + try: import salt.config - from salt.utils import parser as optparse - from salt.utils.process import set_pidfile + from salt.utils import parsers from salt.utils.verify import check_user, verify_env, verify_socket except ImportError as e: if e.args[0] != 'No module named _msgpack': raise -class Master(object): + +class Master(parsers.MasterOptionParser): ''' Creates a master server ''' - def __init__(self): - self.cli = self.__parse_cli() - # command line overrides config - if self.cli['user']: - self.opts['user'] = self.cli['user'] - - # Send the pidfile location to the opts - if self.cli['pidfile']: - self.opts['pidfile'] = self.cli['pidfile'] - - def __parse_cli(self): - ''' - Parse the cli for options passed to a master daemon - ''' - import salt.log - parser = optparse.OptionParser(version="%%prog %s" % __version__) - parser.add_option('-d', - '--daemon', - dest='daemon', - default=False, - action='store_true', - help='Run the master as a daemon') - parser.add_option('-c', - '--config', - dest='config', - default='/etc/salt/master', - help='Pass in an alternative configuration file') - parser.add_option('-u', - '--user', - dest='user', - help='Specify user to run master') - parser.add_option('--pid-file', - dest='pidfile', - help=('Specify the location of the pidfile.')) - parser.add_option('-l', - '--log-level', - dest='log_level', - choices=list(salt.log.LOG_LEVELS), - help='Console log level. One of %s. For the logfile settings ' - 'see the config file. Default: \'warning\'.' % - ', '.join([repr(l) for l in salt.log.SORTED_LEVEL_NAMES])) - - options, args = parser.parse_args() - - self.opts = salt.config.master_config(options.config) - - if not options.log_level: - options.log_level = self.opts['log_level'] - - salt.log.setup_console_logger( - options.log_level, - log_format=self.opts['log_fmt_console'], - date_format=self.opts['log_datefmt'] - ) - - cli = {'daemon': options.daemon, - 'config': options.config, - 'user': options.user, - 'pidfile': options.pidfile} - - return cli def start(self): ''' Run the sequence to start a salt master server ''' + self.parse_args() + try: - verify_env([ - self.opts['pki_dir'], - os.path.join(self.opts['pki_dir'], 'minions'), - os.path.join(self.opts['pki_dir'], 'minions_pre'), - os.path.join(self.opts['pki_dir'], 'minions_rejected'), - self.opts['cachedir'], - os.path.join(self.opts['cachedir'], 'jobs'), - os.path.dirname(self.opts['log_file']), - self.opts['sock_dir'], - ], self.opts['user'], - permissive=self.opts['permissive_pki_access']) + if self.config['verify_env']: + verify_env([ + self.config['pki_dir'], + os.path.join(self.config['pki_dir'], 'minions'), + os.path.join(self.config['pki_dir'], 'minions_pre'), + os.path.join(self.config['pki_dir'], 'minions_rejected'), + self.config['cachedir'], + os.path.join(self.config['cachedir'], 'jobs'), + os.path.dirname(self.config['log_file']), + self.config['sock_dir'], + ], + self.config['user'], + permissive=self.config['permissive_pki_access'], + pki_dir=self.config['pki_dir'], + ) except OSError, err: sys.exit(err.errno) - import salt.log - salt.log.setup_logfile_logger( - self.opts['log_file'], - self.opts['log_level_logfile'] or self.opts['log_level'], - log_format=self.opts['log_fmt_logfile'], - date_format=self.opts['log_datefmt'] - ) - for name, level in self.opts['log_granular_levels'].items(): - salt.log.set_logger_level(name, level) + self.setup_logfile_logger() + logging.getLogger(__name__).warn('Setting up the Salt Master') - import logging - log = logging.getLogger(__name__) # Late import so logging works correctly - if not verify_socket( - self.opts['interface'], - self.opts['publish_port'], - self.opts['ret_port'] - ): - log.critical('The ports are not available to bind') - sys.exit(4) + if not verify_socket(self.config['interface'], + self.config['publish_port'], + self.config['ret_port']): + self.exit(4, 'The ports are not available to bind\n') import salt.master - master = salt.master.Master(self.opts) - if self.cli['daemon']: - # Late import so logging works correctly - import salt.utils - salt.utils.daemonize() - set_pidfile(self.opts['pidfile']) - if check_user(self.opts['user'], log): + master = salt.master.Master(self.config) + self.daemonize_if_required() + self.set_pidfile() + if check_user(self.config['user']): try: master.start() except salt.master.MasterExit: sys.exit() -class Minion(object): +class Minion(parsers.MinionOptionParser): ''' Create a minion server ''' - def __init__(self): - self.cli = self.__parse_cli() - # command line overrides config - if self.cli['user']: - self.opts['user'] = self.cli['user'] - - def __parse_cli(self): - ''' - Parse the cli input - ''' - import salt.log - parser = optparse.OptionParser(version="%%prog %s" % __version__) - parser.add_option('-d', - '--daemon', - dest='daemon', - default=False, - action='store_true', - help='Run the minion as a daemon') - parser.add_option('-c', - '--config', - dest='config', - default='/etc/salt/minion', - help='Pass in an alternative configuration file') - parser.add_option('-u', - '--user', - dest='user', - help='Specify user to run minion') - parser.add_option('--pid-file', - dest='pidfile', - default='/var/run/salt-minion.pid', - help=('Specify the location of the pidfile. Default' - ' %default')) - parser.add_option('-l', - '--log-level', - dest='log_level', - choices=list(salt.log.LOG_LEVELS), - help='Console log level. One of %s. For the logfile settings ' - 'see the config file. Default: \'warning\'.' % - ', '.join([repr(l) for l in salt.log.SORTED_LEVEL_NAMES])) - - options, args = parser.parse_args() - - self.opts = salt.config.minion_config(options.config) - - if not options.log_level: - options.log_level = self.opts['log_level'] - - salt.log.setup_console_logger( - options.log_level, - log_format=self.opts['log_fmt_console'], - date_format=self.opts['log_datefmt'] - ) - - cli = {'daemon': options.daemon, - 'config': options.config, - 'user': options.user, - 'pidfile': options.pidfile} - - return cli def start(self): ''' Execute this method to start up a minion. ''' + self.parse_args() + try: - verify_env([ - self.opts['pki_dir'], - self.opts['cachedir'], - self.opts['sock_dir'], - self.opts['extension_modules'], - os.path.dirname(self.opts['log_file']), - ], self.opts['user'], - permissive=self.opts['permissive_pki_access']) + if self.config['verify_env']: + verify_env([ + self.config['pki_dir'], + self.config['cachedir'], + self.config['sock_dir'], + self.config['extension_modules'], + os.path.dirname(self.config['log_file']), + ], + self.config['user'], + permissive=self.config['permissive_pki_access'], + pki_dir=self.config['pki_dir'], + ) except OSError, err: sys.exit(err.errno) - import salt.log - salt.log.setup_logfile_logger( - self.opts['log_file'], - self.opts['log_level_logfile'] or self.opts['log_level'], - log_format=self.opts['log_fmt_logfile'], - date_format=self.opts['log_datefmt'] + self.setup_logfile_logger() + logging.getLogger(__name__).warn( + 'Setting up the Salt Minion "{0}"'.format( + self.config['id'] + ) ) - for name, level in self.opts['log_granular_levels'].items(): - salt.log.set_logger_level(name, level) - import logging # Late import so logging works correctly import salt.minion - log = logging.getLogger(__name__) - if self.cli['daemon']: - # Late import so logging works correctly - import salt.utils - # If the minion key has not been accepted, then Salt enters a loop - # waiting for it, if we daemonize later then the minion could halt - # the boot process waiting for a key to be accepted on the master. - # This is the latest safe place to daemonize - salt.utils.daemonize() + # If the minion key has not been accepted, then Salt enters a loop + # waiting for it, if we daemonize later then the minion could halt + # the boot process waiting for a key to be accepted on the master. + # This is the latest safe place to daemonize + self.daemonize_if_required() try: - minion = salt.minion.Minion(self.opts) - set_pidfile(self.cli['pidfile']) - if check_user(self.opts['user'], log): + minion = salt.minion.Minion(self.config) + self.set_pidfile() + if check_user(self.config['user']): minion.tune_in() except KeyboardInterrupt: log.warn('Stopping the Salt Minion') raise SystemExit('\nExiting on Ctrl-c') -class Syndic(object): +class Syndic(parsers.SyndicOptionParser): ''' Create a syndic server ''' - def __init__(self): - self.cli = self.__parse_cli() - # command line overrides config - if self.cli['user']: - self.opts['user'] = self.cli['user'] - - def __prep_opts(self, cli): - ''' - Generate the opts used by the syndic - ''' - opts = salt.config.master_config(cli['master_config']) - opts['_minion_conf_file'] = opts['conf_file'] - opts.update(salt.config.minion_config(cli['minion_config'])) - if 'syndic_master' in opts: - # Some of the opts need to be changed to match the needed opts - # in the minion class. - opts['master'] = opts['syndic_master'] - opts['master_ip'] = salt.utils.dns_check(opts['master']) - - opts['master_uri'] = ('tcp://' + opts['master_ip'] + - ':' + str(opts['master_port'])) - opts['_master_conf_file'] = opts['conf_file'] - opts.pop('conf_file') - return opts - err = ('The syndic_master needs to be configured in the salt master ' - 'config, EXITING!\n') - sys.stderr.write(err) - sys.exit(2) - - def __parse_cli(self): - ''' - Parse the cli for options passed to a syndic daemon - ''' - import salt.log - parser = optparse.OptionParser(version="%%prog %s" % __version__) - parser.add_option('-d', - '--daemon', - dest='daemon', - default=False, - action='store_true', - help='Run the syndic as a daemon') - parser.add_option('--master-config', - dest='master_config', - default='/etc/salt/master', - help='Pass in an alternative master configuration file') - parser.add_option('--minion-config', - dest='minion_config', - default='/etc/salt/minion', - help='Pass in an alternative minion configuration file') - parser.add_option('-u', - '--user', - dest='user', - help='Specify user to run syndic') - parser.add_option('--pid-file', - dest='pidfile', - default='/var/run/salt-syndic.pid', - help=('Specify the location of the pidfile. Default' - ' %default')) - parser.add_option('-l', - '--log-level', - dest='log_level', - choices=list(salt.log.LOG_LEVELS), - help='Console log level. One of %s. For the logfile settings ' - 'see the config file. Default: \'warning\'.' % - ', '.join([repr(l) for l in salt.log.LOG_LEVELS])) - - options, args = parser.parse_args() - - cli = {'daemon': options.daemon, - 'minion_config': options.minion_config, - 'master_config': options.master_config, - 'pidfile': options.pidfile, - 'user': options.user} - - self.opts = self.__prep_opts(cli) - - if not options.log_level: - options.log_level = self.opts['log_level'] - - salt.log.setup_console_logger( - options.log_level, - log_format=self.opts['log_fmt_console'], - date_format=self.opts['log_datefmt'] - ) - - return cli def start(self): ''' Execute this method to start up a syndic. ''' + self.parse_args() try: - verify_env([ - self.opts['pki_dir'], self.opts['cachedir'], - os.path.dirname(self.opts['log_file']), - ], self.opts['user'], - permissive=self.opts['permissive_pki_access']) + if self.config['verify_env']: + verify_env([ + self.config['pki_dir'], self.config['cachedir'], + os.path.dirname(self.config['log_file']), + ], + self.config['user'], + permissive=self.config['permissive_pki_access'], + pki_dir=self.config['pki_dir'], + ) except OSError, err: sys.exit(err.errno) - import salt.log - salt.log.setup_logfile_logger( - self.opts['log_file'], self.opts['log_level'] - ) - for name, level in self.opts['log_granular_levels'].items(): - salt.log.set_logger_level(name, level) - import logging + self.setup_logfile_logger() + logging.getLogger(__name__).warn( + 'Setting up the Salt Syndic Minion "{0}"'.format( + self.config['id'] + ) + ) # Late import so logging works correctly import salt.minion - log = logging.getLogger(__name__) - if self.cli['daemon']: - # Late import so logging works correctly - import salt.utils - salt.utils.daemonize() - set_pidfile(self.cli['pidfile']) - if check_user(self.opts['user'], log): + self.daemonize_if_required() + self.set_pidfile() + + if check_user(self.config['user']): try: - syndic = salt.minion.Syndic(self.opts) + syndic = salt.minion.Syndic(self.config) syndic.tune_in() except KeyboardInterrupt: log.warn('Stopping the Salt Syndic Minion') diff --git a/salt/_compat.py b/salt/_compat.py index 37352e6962..7d19322afd 100644 --- a/salt/_compat.py +++ b/salt/_compat.py @@ -38,16 +38,20 @@ else: def text_(s, encoding='latin-1', errors='strict'): - """ If ``s`` is an instance of ``binary_type``, return - ``s.decode(encoding, errors)``, otherwise return ``s``""" + ''' + If ``s`` is an instance of ``binary_type``, return + ``s.decode(encoding, errors)``, otherwise return ``s`` + ''' if isinstance(s, binary_type): return s.decode(encoding, errors) return s def bytes_(s, encoding='latin-1', errors='strict'): - """ If ``s`` is an instance of ``text_type``, return - ``s.encode(encoding, errors)``, otherwise return ``s``""" + ''' + If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``s`` + ''' if isinstance(s, text_type): return s.encode(encoding, errors) return s @@ -64,37 +68,41 @@ else: s = s.encode('ascii') return str(s) -ascii_native_.__doc__ = """ +ascii_native_.__doc__ = ''' Python 3: If ``s`` is an instance of ``text_type``, return ``s.encode('ascii')``, otherwise return ``str(s, 'ascii', 'strict')`` Python 2: If ``s`` is an instance of ``text_type``, return ``s.encode('ascii')``, otherwise return ``str(s)`` -""" +''' if PY3: def native_(s, encoding='latin-1', errors='strict'): - """ If ``s`` is an instance of ``text_type``, return - ``s``, otherwise return ``str(s, encoding, errors)``""" + ''' + If ``s`` is an instance of ``text_type``, return + ``s``, otherwise return ``str(s, encoding, errors)`` + ''' if isinstance(s, text_type): return s return str(s, encoding, errors) else: def native_(s, encoding='latin-1', errors='strict'): - """ If ``s`` is an instance of ``text_type``, return - ``s.encode(encoding, errors)``, otherwise return ``str(s)``""" + ''' + If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``str(s)`` + ''' if isinstance(s, text_type): return s.encode(encoding, errors) return str(s) -native_.__doc__ = """ +native_.__doc__ = ''' Python 3: If ``s`` is an instance of ``text_type``, return ``s``, otherwise return ``str(s, encoding, errors)`` Python 2: If ``s`` is an instance of ``text_type``, return ``s.encode(encoding, errors)``, otherwise return ``str(s)`` -""" +''' if PY3: from urllib import parse diff --git a/salt/cli/__init__.py b/salt/cli/__init__.py index 853b4061ff..f9ef7efa0a 100644 --- a/salt/cli/__init__.py +++ b/salt/cli/__init__.py @@ -4,7 +4,6 @@ The management of salt command line utilities are stored in here # Import python libs import os -import sys # Import salt components import salt.cli.caller @@ -15,309 +14,68 @@ import salt.client import salt.output import salt.runner -from salt.utils import parser as optparse +from salt.utils import parsers from salt.utils.verify import verify_env -from salt import __version__ as VERSION -from salt.exceptions import SaltInvocationError, SaltClientError, \ - SaltException +from salt.exceptions import SaltInvocationError, SaltClientError -class SaltCMD(object): +class SaltCMD(parsers.SaltCMDOptionParser): ''' The execution of a salt command happens here ''' - def __init__(self): - ''' - Create a SaltCMD object - ''' - self.opts = self.__parse() - - def __parse(self): - ''' - Parse the command line - ''' - usage = "%prog [options] '' [arguments]" - parser = optparse.OptionParser(version="%%prog %s" % VERSION, usage=usage) - - parser.add_option('-t', - '--timeout', - default=None, - dest='timeout', - help=('Set the return timeout for batch jobs; ' - 'default=5 seconds')) - parser.add_option('-s', - '--static', - default=False, - dest='static', - action='store_true', - help=('Return the data from minions as a group after they ' - 'all return.')) - parser.add_option('-v', - '--verbose', - default=False, - dest='verbose', - action='store_true', - help=('Turn on command verbosity, display jid and active job ' - 'queries')) - parser.add_option('-b', - '--batch', - '--batch-size', - default='', - dest='batch', - help=('Execute the salt job in batch mode, pass either the ' - 'number of minions to batch at a time, or the ' - 'percentage of minions to have running')) - parser.add_option('-E', - '--pcre', - default=False, - dest='pcre', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'servers, use pcre regular expressions')) - parser.add_option('-L', - '--list', - default=False, - dest='list', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'servers, take a comma delimited list of servers.')) - parser.add_option('-G', - '--grain', - default=False, - dest='grain', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a grain value to identify targets, the syntax ' - 'for the target is the grain key followed by a glob' - 'expression:\n"os:Arch*"')) - parser.add_option('--grain-pcre', - default=False, - dest='grain_pcre', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a grain value to identify targets, the syntax ' - 'for the target is the grain key followed by a pcre ' - 'regular expression:\n"os:Arch.*"')) - parser.add_option('-X', - '--exsel', - default=False, - dest='exsel', - action='store_true', - help=('Instead of using shell globs use the return code ' - 'of a function.')) - parser.add_option('-I', - '--pillar', - default=False, - dest='pillar', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a pillar value to identify targets, the syntax ' - 'for the target is the pillar key followed by a glob' - 'expression:\n"role:production*"')) - parser.add_option('-N', - '--nodegroup', - default=False, - dest='nodegroup', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use one of the predefined nodegroups to identify a ' - 'list of targets.')) - parser.add_option('-R', - '--range', - default=False, - dest='range', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a range expression to identify targets. ' - 'Range expressions look like %cluster')) - parser.add_option('-C', - '--compound', - default=False, - dest='compound', - action='store_true', - help=('The compound target option allows for multiple ' - 'target types to be evaluated, allowing for greater ' - 'granularity in target matching. The compound target ' - 'is space delimited, targets other than globs are ' - 'preceted with an identifyer matching the specific ' - 'targets argument type: salt \'G@os:RedHat and ' - 'webser* or E@database.*\'')) - parser.add_option('--return', - default='', - dest='return', - metavar='RETURNER', - help=('Set an alternative return method. By default salt will ' - 'send the return data from the command back to the ' - 'master, but the return data can be redirected into ' - 'any number of systems, databases or applications.')) - parser.add_option('-Q', - '--query', - dest='query', - action='store_true', - help=('This option is deprecated and will be removed in a ' - 'future release, please use salt-run jobs instead\n' - 'Execute a salt command query, this can be used to find ' - 'the results of a previous function call: -Q test.echo')) - parser.add_option('-c', - '--config', - default='/etc/salt/master', - dest='conf_file', - help=('The location of the salt master configuration file, ' - 'the salt master settings are required to know where ' - 'the connections are; default=/etc/salt/master')) - parser.add_option('--raw-out', - default=False, - action='store_true', - dest='raw_out', - help=('Print the output from the salt command in raw python ' - 'form, this is suitable for re-reading the output into ' - 'an executing python script with eval.')) - parser.add_option('--text-out', - default=False, - action='store_true', - dest='txt_out', - help=('Print the output from the salt command in the same ' - 'form the shell would.')) - parser.add_option('--yaml-out', - default=False, - action='store_true', - dest='yaml_out', - help='Print the output from the salt command in yaml.') - parser.add_option('--json-out', - default=False, - action='store_true', - dest='json_out', - help='Print the output from the salt command in json.') - parser.add_option('--no-color', - default=False, - action='store_true', - dest='no_color', - help='Disable all colored output') - - options, args = parser.parse_args() - - opts = {} - - for k, v in options.__dict__.items(): - if v is not None: - opts[k] = v - - if not options.timeout is None: - opts['timeout'] = int(options.timeout) - - if options.query: - opts['query'] = options.query - if len(args) < 1: - err = ('Please pass in a command to query the old salt ' - 'calls for.') - sys.stderr.write(err + '\n') - sys.exit('2') - opts['cmd'] = args[0] - else: - # Catch invalid invocations of salt such as: salt run - if len(args) <= 1: - parser.print_help() - parser.exit(1) - - if opts['list']: - opts['tgt'] = args[0].split(',') - else: - opts['tgt'] = args[0] - - # Detect compound command and set up the data for it - if ',' in args[1]: - opts['fun'] = args[1].split(',') - opts['arg'] = [] - for comp in ' '.join(args[2:]).split(','): - opts['arg'].append(comp.split()) - if len(opts['fun']) != len(opts['arg']): - err = ('Cannot execute compound command without defining ' - 'all arguments.') - sys.stderr.write(err + '\n') - sys.exit(42) - else: - opts['fun'] = args[1] - opts['arg'] = args[2:] - - return opts def run(self): ''' Execute the salt command line ''' + self.parse_args() + try: - local = salt.client.LocalClient(self.opts['conf_file']) + local = salt.client.LocalClient(self.get_config_file_path('master')) except SaltClientError as exc: - sys.stderr.write('{0}\n'.format(exc)) - sys.exit(2) + self.exit(2, '{0}\n'.format(exc)) return - if 'query' in self.opts: - ret = local.find_cmd(self.opts['cmd']) + if self.options.query: + ret = local.find_cmd(self.config['cmd']) for jid in ret: if isinstance(ret, list) or isinstance(ret, dict): - # Determine the proper output method and run it - get_outputter = salt.output.get_outputter - if self.opts['raw_out']: - printout = get_outputter('raw') - elif self.opts['json_out']: - printout = get_outputter('json') - elif self.opts['txt_out']: - printout = get_outputter('txt') - elif self.opts['yaml_out']: - printout = get_outputter('yaml') - else: - printout = get_outputter(None) - print('Return data for job {0}:'.format(jid)) - printout(ret[jid]) + salt.output.display_output(ret[jid], None, self.config) print('') - elif self.opts['batch']: - batch = salt.cli.batch.Batch(self.opts) + elif self.options.batch: + batch = salt.cli.batch.Batch(self.config) batch.run() else: - if not 'timeout' in self.opts: - self.opts['timeout'] = local.opts['timeout'] - args = [self.opts['tgt'], - self.opts['fun'], - self.opts['arg'], - self.opts['timeout'], - ] - if self.opts['pcre']: - args.append('pcre') - elif self.opts['list']: - args.append('list') - elif self.opts['grain']: - args.append('grain') - elif self.opts['grain_pcre']: - args.append('grain_pcre') - elif self.opts['exsel']: - args.append('exsel') - elif self.opts['pillar']: - args.append('pillar') - elif self.opts['nodegroup']: - args.append('nodegroup') - elif self.opts['range']: - args.append('range') - elif self.opts['compound']: - args.append('compound') + if self.options.timeout <= 0: + self.options.timeout = local.opts['timeout'] + + args = [ + self.config['tgt'], + self.config['fun'], + self.config['arg'], + self.options.timeout, + ] + + if self.selected_target_option: + args.append(self.selected_target_option) else: args.append('glob') - if self.opts['return']: - args.append(self.opts['return']) + if getattr(self.options, 'return'): + args.append(getattr(self.options, 'return')) else: args.append('') try: # local will be None when there was an error if local: - if self.opts['static']: - if self.opts['verbose']: + if self.options.static: + if self.options.verbose: args.append(True) full_ret = local.cmd_full_return(*args) ret, out = self._format_ret(full_ret) self._output_ret(ret, out) - elif self.opts['fun'] == 'sys.doc': + elif self.config['fun'] == 'sys.doc': ret = {} out = '' for full_ret in local.cmd_cli(*args): @@ -325,7 +83,7 @@ class SaltCMD(object): ret.update(ret_) self._output_ret(ret, out) else: - if self.opts['verbose']: + if self.options.verbose: args.append(True) for full_ret in local.cmd_cli(*args): ret, out = self._format_ret(full_ret) @@ -339,29 +97,11 @@ class SaltCMD(object): Print the output from a single return to the terminal ''' # Handle special case commands - if self.opts['fun'] == 'sys.doc': + if self.config['fun'] == 'sys.doc': self._print_docs(ret) else: # Determine the proper output method and run it - get_outputter = salt.output.get_outputter - if isinstance(ret, list) or isinstance(ret, dict): - if self.opts['raw_out']: - printout = get_outputter('raw') - elif self.opts['json_out']: - printout = get_outputter('json') - elif self.opts['txt_out']: - printout = get_outputter('txt') - elif self.opts['yaml_out']: - printout = get_outputter('yaml') - elif out: - printout = get_outputter(out) - else: - printout = get_outputter(None) - # Pretty print any salt exceptions - elif isinstance(ret, SaltException): - printout = get_outputter("txt") - color = not bool(self.opts['no_color']) - printout(ret, color=color) + salt.output.display_output(ret, out, self.config) def _format_ret(self, full_ret): ''' @@ -381,7 +121,8 @@ class SaltCMD(object): ''' docs = {} if not ret: - sys.stderr.write('No minions found to gather docs from\n') + self.exit(2, 'No minions found to gather docs from\n') + for host in ret: for fun in ret[host]: if fun not in docs: @@ -393,485 +134,104 @@ class SaltCMD(object): print('') -class SaltCP(object): +class SaltCP(parsers.SaltCPOptionParser): ''' Run the salt-cp command line client ''' - def __init__(self): - self.opts = self.__parse() - - def __parse(self): - ''' - Parse the command line - ''' - usage = "%prog [options] '' SOURCE DEST" - parser = optparse.OptionParser(version="%%prog %s" % VERSION, usage=usage) - - parser.add_option('-t', - '--timeout', - default=5, - type=int, - dest='timeout', - help=('Set the return timeout for batch jobs; ' - 'default=5 seconds')) - parser.add_option('-E', - '--pcre', - default=False, - dest='pcre', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'servers, use pcre regular expressions')) - parser.add_option('-L', - '--list', - default=False, - dest='list', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'servers, take a comma delimited list of servers.')) - parser.add_option('-G', - '--grain', - default=False, - dest='grain', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a grain value to identify targets, the syntax ' - 'for the target is the grain key followed by a glob' - 'expression:\n"os:Arch*"')) - parser.add_option('--grain-pcre', - default=False, - dest='grain_pcre', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a grain value to identify targets, the syntax ' - 'for the target is the grain key followed by a pcre ' - 'regular expression:\n"os:Arch.*"')) - parser.add_option('-N', - '--nodegroup', - default=False, - dest='nodegroup', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use one of the predefined nodegroups to identify a ' - 'list of targets.')) - parser.add_option('-R', - '--range', - default=False, - dest='range', - action='store_true', - help=('Instead of using shell globs to evaluate the target ' - 'use a range expressions to identify targets. ' - 'Range expressions look like %cluster')) - parser.add_option('-c', - '--config', - default='/etc/salt/master', - dest='conf_file', - help=('The location of the salt master configuration file, ' - 'the salt master settings are required to know where ' - 'the connections are; default=/etc/salt/master')) - - options, args = parser.parse_args() - - opts = {} - - for k, v in options.__dict__.items(): - if v is not None: - opts[k] = v - - # salt-cp needs arguments - if len(args) <= 1: - parser.print_help() - parser.exit(1) - - if opts['list']: - opts['tgt'] = args[0].split(',') - else: - opts['tgt'] = args[0] - opts['src'] = args[1:-1] - opts['dest'] = args[-1] - - return opts def run(self): ''' Execute salt-cp ''' - cp_ = salt.cli.cp.SaltCP(self.opts) + self.parse_args() + cp_ = salt.cli.cp.SaltCP(self.config) cp_.run() -class SaltKey(object): +class SaltKey(parsers.SaltKeyOptionParser): ''' Initialize the Salt key manager ''' - def __init__(self): - self.opts = self.__parse() - - def __parse(self): - ''' - Parse the command line options for the salt key - ''' - parser = optparse.OptionParser(version="%%prog %s" % VERSION) - - parser.add_option('-l', - '--list', - dest='list', - default='', - help=('List the public keys. Takes the args: ' - '"pre", "un", "unaccepted": Unaccepted/unsigned keys ' - '"acc", "accepted": Accepted/signed keys ' - '"rej", "rejected": Rejected keys ' - '"all": all keys')) - - parser.add_option('-L', - '--list-all', - dest='list_all', - default=False, - action='store_true', - help='List all public keys. Deprecated: use "--list all"') - - parser.add_option('-a', - '--accept', - dest='accept', - default='', - help='Accept the following key') - - parser.add_option('-A', - '--accept-all', - dest='accept_all', - default=False, - action='store_true', - help='Accept all pending keys') - - parser.add_option('-r', - '--reject', - dest='reject', - default='', - help='Reject the specified public key') - - parser.add_option('-R', - '--reject-all', - dest='reject_all', - default=False, - action='store_true', - help='Reject all pending keys') - - parser.add_option('-p', - '--print', - dest='print', - default='', - help='Print the specified public key') - - parser.add_option('-P', - '--print-all', - dest='print_all', - default=False, - action='store_true', - help='Print all public keys') - - parser.add_option('-d', - '--delete', - dest='delete', - default='', - help='Delete the named key') - - parser.add_option('-D', - '--delete-all', - dest='delete_all', - default=False, - action='store_true', - help='Delete all keys') - - parser.add_option('-q', - '--quiet', - dest='quiet', - default=False, - action='store_true', - help='Supress output') - - parser.add_option('-y', - '--yes', - dest='yes', - default=False, - action='store_true', - help='Answer Yes to all questions presented, defaults to False' - ) - - parser.add_option('--key-logfile', - dest='key_logfile', - help=('Send all output to a file. ' - 'Default is /var/log/salt/key')) - - parser.add_option('--gen-keys', - dest='gen_keys', - default='', - help='Set a name to generate a keypair for use with salt') - - parser.add_option('--gen-keys-dir', - dest='gen_keys_dir', - default='.', - help=('Set the direcotry to save the generated keypair, ' - 'only works with "gen_keys_dir" option; default=.')) - - parser.add_option('--keysize', - dest='keysize', - default=2048, - type=int, - help=('Set the keysize for the generated key, only works with ' - 'the "--gen-keys" option, the key size must be 2048 or ' - 'higher, otherwise it will be rounded up to 2048' - '; default=2048')) - - parser.add_option('-c', - '--config', - dest='conf_file', - default='/etc/salt/master', - help='Pass in an alternative configuration file') - - parser.add_option('--raw-out', - default=False, - action='store_true', - dest='raw_out', - help=('Print the output from the salt-key command in raw python ' - 'form, this is suitable for re-reading the output into ' - 'an executing python script with eval.')) - - parser.add_option('--yaml-out', - default=False, - action='store_true', - dest='yaml_out', - help='Print the output from the salt-key command in yaml.') - - parser.add_option('--json-out', - default=False, - action='store_true', - dest='json_out', - help='Print the output from the salt-key command in json.') - - parser.add_option('--no-color', - default=False, - action='store_true', - dest='no_color', - help='Disable all colored output') - - options, args = parser.parse_args() - - opts = {} - opts.update(salt.config.master_config(options.conf_file)) - - for k, v in options.__dict__.items(): - if k == 'keysize': - if v < 2048: - opts[k] = 2048 - else: - opts[k] = v - elif v is not None: - opts[k] = v - # I decided to always set this to info, since it really all is info or - # error. - opts['loglevel'] = 'info' - return opts def run(self): ''' - Execute saltkey + Execute salt-key ''' - verify_env([ - os.path.join(self.opts['pki_dir'], 'minions'), - os.path.join(self.opts['pki_dir'], 'minions_pre'), - os.path.join(self.opts['pki_dir'], 'minions_rejected'), - os.path.dirname(self.opts['log_file']), - ], - self.opts['user'], - permissive=self.opts['permissive_pki_access']) - import salt.log - salt.log.setup_logfile_logger(self.opts['key_logfile'], - self.opts['loglevel']) - key = salt.cli.key.Key(self.opts) + self.parse_args() + + if self.config['verify_env']: + verify_env_dirs = [] + if not self.config['gen_keys']: + verify_env_dirs.extend([ + os.path.join(self.config['pki_dir'], 'minions'), + os.path.join(self.config['pki_dir'], 'minions_pre'), + os.path.join(self.config['pki_dir'], 'minions_rejected'), + os.path.dirname(self.config['key_logfile']) + ]) + + verify_env( + verify_env_dirs, + self.config['user'], + permissive=self.config['permissive_pki_access'], + pki_dir=self.config['pki_dir'], + ) + + self.setup_logfile_logger() + + key = salt.cli.key.Key(self.config) key.run() -class SaltCall(object): +class SaltCall(parsers.SaltCallOptionParser): ''' Used to locally execute a salt command ''' - def __init__(self): - self.opts = self.__parse() - - def __parse(self): - ''' - Parse the command line arguments - ''' - usage = "%prog [options] [arguments]" - parser = optparse.OptionParser( - version='salt-call {0}'.format(VERSION), - usage=usage - ) - - parser.add_option('-g', - '--grains', - dest='grains_run', - default=False, - action='store_true', - help='Return the information generated by the salt grains') - parser.add_option('-m', - '--module-dirs', - dest='module_dirs', - default='', - help=('Specify an additional directories to pull modules ' - 'from, multiple directories can be delimited by commas')) - parser.add_option('-c', - '--config', - dest='conf_file', - default='/etc/salt/minion', - help='Pass in an alternative configuration file') - parser.add_option('-d', - '--doc', - dest='doc', - default=False, - action='store_true', - help=('Return the documentation for the specified module of ' - 'for all modules if none are specified')) - parser.add_option('-l', - '--log-level', - default='info', - dest='log_level', - help='Set the output level for salt-call') - parser.add_option('--raw-out', - default=False, - action='store_true', - dest='raw_out', - help=('Print the output from the salt command in raw python ' - 'form, this is suitable for re-reading the output into ' - 'an executing python script with eval.')) - parser.add_option('--text-out', - default=False, - action='store_true', - dest='txt_out', - help=('Print the output from the salt command in the same ' - 'form the shell would.')) - parser.add_option('--yaml-out', - default=False, - action='store_true', - dest='yaml_out', - help='Print the output from the salt command in yaml.') - parser.add_option('--json-out', - default=False, - action='store_true', - dest='json_out', - help='Print the output from the salt command in json.') - parser.add_option('--no-color', - default=False, - dest='no_color', - action='store_true', - help='Disable all colored output') - - options, args = parser.parse_args() - - opts = {} - opts.update(salt.config.minion_config(options.conf_file)) - - for k, v in options.__dict__.items(): - if k == 'module_dirs': - opts[k] = v.split(',') - else: - opts[k] = v - - if len(args) >= 1: - opts['fun'] = args[0] - opts['arg'] = args[1:] - elif opts['grains_run']: - pass - else: - # salt-call should not ever be called without arguments - parser.print_help() - parser.exit(1) - - verify_env([opts['pki_dir'], - opts['cachedir'], - os.path.dirname(opts['log_file']), - ], - opts['user'], - permissive=opts['permissive_pki_access']) - - return opts def run(self): ''' Execute the salt call! ''' - import salt.log - salt.log.setup_console_logger( - self.opts['log_level'], - log_format=self.opts['log_fmt_console'], - date_format=self.opts['log_datefmt'], - ) - caller = salt.cli.caller.Caller(self.opts) + self.parse_args() + + if self.config['verify_env']: + verify_env([ + self.config['pki_dir'], + self.config['cachedir'], + os.path.dirname(self.config['log_file']) + ], + self.config['user'], + permissive=self.config['permissive_pki_access'], + pki_dir=self.config['pki_dir'], + ) + + caller = salt.cli.caller.Caller(self.config) + + if self.options.doc: + caller.print_docs() + self.exit(0) + + if self.options.grains_run: + caller.print_grains() + self.exit(0) + caller.run() -class SaltRun(object): - ''' - Used to execute salt convenience functions - ''' - def __init__(self): - self.opts = self.__parse() - - def __parse(self): - ''' - Parse the command line arguments - ''' - parser = optparse.OptionParser(version="%%prog %s" % VERSION) - - parser.add_option('-c', - '--config', - dest='conf_file', - default='/etc/salt/master', - help=('Change the location of the master configuration; ' - 'default=/etc/salt/master')) - - parser.add_option('-t', - '--timeout', - dest='timeout', - default='1', - help=('Change the timeout, if applicable, for the salt runner; ' - 'default=1')) - - parser.add_option('-d', - '--doc', - '--documentation', - dest='doc', - default=False, - action='store_true', - help=('Display documentation for runners, pass a module or ' - 'a runner to see documentation on only that ' - 'module/runner.')) - - options, args = parser.parse_args() - - opts = {} - opts.update(salt.config.master_config(options.conf_file)) - opts['conf_file'] = options.conf_file - opts['doc'] = options.doc - if len(args) > 0: - opts['fun'] = args[0] - else: - opts['fun'] = '' - if len(args) > 1: - opts['arg'] = args[1:] - else: - opts['arg'] = [] - - return opts +class SaltRun(parsers.SaltRunOptionParser): def run(self): ''' Execute salt-run ''' - runner = salt.runner.Runner(self.opts) - # Run this here so SystemExit isn't raised - # anywhere else when someone tries to use - # the runners via the python api - try: - runner.run() - except SaltClientError as exc: - raise SystemExit(str(exc)) + self.parse_args() + + runner = salt.runner.Runner(self.config) + if self.options.doc: + runner._print_docs() + else: + # Run this here so SystemExit isn't raised anywhere else when + # someone tries to use the runners via the python api + try: + runner.run() + except SaltClientError as exc: + raise SystemExit(str(exc)) diff --git a/salt/cli/batch.py b/salt/cli/batch.py index 5feef7506d..83517c49b2 100644 --- a/salt/cli/batch.py +++ b/salt/cli/batch.py @@ -29,22 +29,10 @@ class Batch(object): [], 1, ] - if self.opts['pcre']: - args.append('pcre') - elif self.opts['list']: - args.append('list') - elif self.opts['grain']: - args.append('grain') - elif self.opts['grain_pcre']: - args.append('grain_pcre') - elif self.opts['exsel']: - args.append('exsel') - elif self.opts['pillar']: - args.append('pillar') - elif self.opts['nodegroup']: - args.append('nodegroup') - elif self.opts['compound']: - args.append('compound') + + selected_target_option = self.opts.get('selected_target_option', None) + if selected_target_option is not None: + args.append(selected_target_option) else: args.append('glob') diff --git a/salt/cli/caller.py b/salt/cli/caller.py index 4929a59cd2..e4756b6aad 100644 --- a/salt/cli/caller.py +++ b/salt/cli/caller.py @@ -9,10 +9,9 @@ import logging import traceback # Import salt libs -import salt -import salt.utils import salt.loader import salt.minion +import salt.output from salt._compat import string_types from salt.log import LOG_LEVELS @@ -83,7 +82,7 @@ class Caller(object): if func.__doc__: docs[name] = func.__doc__ for name in sorted(docs): - if name.startswith(self.opts['fun']): + if name.startswith(self.opts.get('fun', '')): print('{0}:\n{1}\n'.format(name, docs[name])) def print_grains(self): @@ -91,44 +90,20 @@ class Caller(object): Print out the grains ''' grains = salt.loader.grains(self.opts) - printout = self._get_outputter(out='yaml') - # If --json-out is specified, pretty print it - if 'json_out' in self.opts and self.opts['json_out']: - printout.indent = 2 - printout(grains) - - def _get_outputter(self, out=None): - get_outputter = salt.output.get_outputter - if self.opts['raw_out']: - printout = get_outputter('raw') - elif self.opts['json_out']: - printout = get_outputter('json') - elif self.opts['txt_out']: - printout = get_outputter('txt') - elif self.opts['yaml_out']: - printout = get_outputter('yaml') - elif out: - printout = get_outputter(out) - else: - printout = get_outputter(None) - return printout + printout = salt.output.get_printout(grains, 'yaml', self.opts, indent=2) + printout(grains, color=not bool(self.opts['no_color'])) def run(self): ''' Execute the salt call logic ''' - if self.opts['doc']: - self.print_docs() - elif self.opts['grains_run']: - self.print_grains() - else: - ret = self.call() - # Determine the proper output method and run it - if 'out' in ret: - printout = self._get_outputter(ret['out']) - else: - printout = self._get_outputter() - if 'json_out' in self.opts and self.opts['json_out']: - printout.indent = 2 - color = not bool(self.opts['no_color']) - printout({'local': ret['return']}, color=color) + ret = self.call() + printout = salt.output.get_printout( + ret, ret.get('out', None), self.opts, indent=2 + ) + if printout is None: + printout = salt.output.get_outputter(None) + printout( + {'local': ret['return']}, + color=not bool(self.opts['no_color']), + **self.opts) diff --git a/salt/cli/cp.py b/salt/cli/cp.py index 450bce06db..05a362d18d 100644 --- a/salt/cli/cp.py +++ b/salt/cli/cp.py @@ -71,18 +71,10 @@ class SaltCP(object): arg, self.opts['timeout'], ] - if self.opts['pcre']: - args.append('pcre') - elif self.opts['list']: - args.append('list') - elif self.opts['grain']: - args.append('grain') - elif self.opts['grain_pcre']: - args.append('grain_pcre') - elif self.opts['nodegroup']: - args.append('nodegroup') - elif self.opts['range']: - args.append('range') + + selected_target_option = self.opts.get('selected_target_option', None) + if selected_target_option is not None: + args.append(selected_target_option) ret = local.cmd(*args) diff --git a/salt/cli/key.py b/salt/cli/key.py index 14ea4edc4f..2a10d4559f 100644 --- a/salt/cli/key.py +++ b/salt/cli/key.py @@ -21,10 +21,27 @@ class Key(object): ''' def __init__(self, opts): self.opts = opts - self.event = salt.utils.event.SaltEvent(opts['sock_dir'], 'master') + self.event = salt.utils.event.MasterEvent(opts['sock_dir']) self.colors = salt.utils.get_colors( not bool(self.opts.get('no_color', False)) ) + if not opts.get('gen_keys', None): + # Only check for a master running IF we need it. + # While generating keys we don't + self._check_master() + + def _check_master(self): + ''' + Log if the master is not running + ''' + if not os.path.exists( + os.path.join( + self.opts['sock_dir'], + 'publish_pull.ipc' + ) + ): + self._log('Master is not running', level='error') + def _cli_opts(self, **kwargs): ''' @@ -42,7 +59,8 @@ class Key(object): 'print_all': False, 'delete': '', 'delete_all': False, - 'quiet': Fasle, + 'finger': '', + 'quiet': False, 'yes': True, 'gen_keys': '', 'gen_keys_dir': '.', @@ -87,7 +105,7 @@ class Key(object): if hasattr(log, level): log_msg = getattr(log, level) log_msg(message) - if not self.opts['quiet']: + if not self.opts.get('quiet', False): print(message) def _list_pre(self, header=True, printer=None): @@ -148,9 +166,11 @@ class Key(object): ''' List keys ''' - printout = self._get_outputter() - if 'json_out' in self.opts and self.opts['json_out']: - printout.indent = 2 + selected_output = self.opts.get('selected_output_option', None) + printout = salt.output.get_printout( + {}, selected_output, self.opts, indent=2 + ) + if name in ('pre', 'un', 'unaccept', 'unaccepted'): self._list_pre(header=False, printer=printout) elif name in ('acc', 'accept', 'accepted'): @@ -174,18 +194,6 @@ class Key(object): ).format(name) self._log(err, level='error') - def _get_outputter(self): - get_outputter = salt.output.get_outputter - if self.opts['raw_out']: - printout = get_outputter('raw') - elif self.opts['json_out']: - printout = get_outputter('json') - elif self.opts['yaml_out']: - printout = get_outputter('yaml') - else: - printout = None # use default color output - return printout - def _print_key(self, name): ''' Print out the specified public key @@ -330,7 +338,6 @@ class Key(object): Delete all keys ''' # Don't ask for verification if yes is not set - yes = self.opts.get('yes', True) del_ = set() for dir in ("acc", "rej", "pre"): for key in self._keys(dir): @@ -372,6 +379,9 @@ class Key(object): self._reject(key) def _check_minions_directories(self): + ''' + Return the minion keys directory paths + ''' minions_accepted = os.path.join(self.opts['pki_dir'], 'minions') minions_pre = os.path.join(self.opts['pki_dir'], 'minions_pre') minions_rejected = os.path.join(self.opts['pki_dir'], @@ -384,6 +394,32 @@ class Key(object): sys.exit(42) return minions_accepted, minions_pre, minions_rejected + def finger(self): + ''' + Return the fingerprint for a specified key + ''' + fkey = self.opts.get('finger', 'master') + dirs = list(self._check_minions_directories()) + dirs.append(self.opts['pki_dir']) + sigs = {} + for dir_ in dirs: + pub = os.path.join(dir_, '{0}.pub'.format(fkey)) + fin = salt.utils.pem_finger(pub) + if fin: + self._log('Signature for {0} public key: {1}'.format(fkey, fin)) + sigs['{0}.pub'.format(fkey)] = fin + pub = os.path.join(dir_, '{0}'.format(fkey)) + fin = salt.utils.pem_finger(pub) + if fin: + self._log('Signature for {0} public key: {1}'.format(fkey, fin)) + sigs['{0}.pub'.format(fkey)] = fin + pri = os.path.join(dir_, '{0}.pem'.format(fkey)) + fin = salt.utils.pem_finger(pri) + if fin: + self._log('Signature for {0} private key: {1}'.format(fkey, fin)) + sigs['{0}.pem'.format(fkey)] = fin + return sigs + def run(self): ''' Run the logic for saltkey @@ -393,6 +429,7 @@ class Key(object): self.opts['gen_keys_dir'], self.opts['gen_keys'], self.opts['keysize']) + self._log('Keys generation complete', level='info') return if self.opts['list']: self._list(self.opts['list']) @@ -414,5 +451,7 @@ class Key(object): self._delete_key() elif self.opts['delete_all']: self._delete_all() + elif self.opts['finger']: + self.finger() else: self._list('all') diff --git a/salt/client.py b/salt/client.py index d6521c579a..65ecae3ecb 100644 --- a/salt/client.py +++ b/salt/client.py @@ -36,9 +36,6 @@ import time import getpass import fnmatch -# Import zmq modules -import zmq - # Import salt modules import salt.config import salt.payload @@ -75,18 +72,22 @@ class LocalClient(object): def __init__(self, c_path='/etc/salt/master'): self.opts = salt.config.master_config(c_path) self.serial = salt.payload.Serial(self.opts) - self.key = self.__read_master_key() self.salt_user = self.__get_user() + self.key = self.__read_master_key() self.event = salt.utils.event.MasterEvent(self.opts['sock_dir']) def __read_master_key(self): ''' Read in the rotating master authentication key ''' - keyfile = os.path.join(self.opts['cachedir'], '.root_key') + key_user = self.salt_user + if key_user.startswith('sudo_'): + key_user = 'root' + keyfile = os.path.join( + self.opts['cachedir'], '.{0}_key'.format(key_user) + ) # Make sure all key parent directories are accessible - user = self.opts.get('user', 'root') - salt.utils.verify.check_parent_dirs(keyfile, user) + salt.utils.verify.check_parent_dirs(keyfile, key_user) try: with open(keyfile, 'r') as KEY: @@ -107,12 +108,12 @@ class LocalClient(object): env_vars = ['SUDO_USER', 'USER', 'USERNAME'] for evar in env_vars: if evar in os.environ: - return os.environ[evar] - return None + return 'sudo_{0}'.format(os.environ[evar]) + return user # If the running user is just the specified user in the # conf file, don't pass the user as it's implied. elif user == self.opts['user']: - return None + return user return user def _check_glob_minions(self, expr): @@ -120,13 +121,7 @@ class LocalClient(object): Return the minions found by looking via globs ''' cwd = os.getcwd() - try: - os.chdir(os.path.join(self.opts['pki_dir'], 'minions')) - except OSError: - err = ('The Salt Master has not been set up on this system, ' - 'a salt-master needs to be running to use the salt command') - sys.stderr.write(err) - sys.exit(2) + os.chdir(os.path.join(self.opts['pki_dir'], 'minions')) ret = set(glob.glob(expr)) os.chdir(cwd) return ret @@ -177,18 +172,20 @@ class LocalClient(object): continue if comps[0] not in grains: minions.remove(id_) - if isinstance(grains[comps[0]], list): + continue + if isinstance(grains.get(comps[0]), list): # We are matching a single component to a single list member found = False for member in grains[comps[0]]: if fnmatch.fnmatch(str(member).lower(), comps[1].lower()): found = True + break if found: continue minions.remove(id_) continue if fnmatch.fnmatch( - str(grains[comps[0]]).lower(), + str(grains.get(comps[0], '').lower()), comps[1].lower(), ): continue @@ -276,10 +273,13 @@ class LocalClient(object): arg = condition_kwarg(arg, kwarg) if timeout is None: timeout = self.opts['timeout'] - jid = salt.utils.prep_jid( - self.opts['cachedir'], - self.opts['hash_type'] - ) + try: + jid = salt.utils.prep_jid( + self.opts['cachedir'], + self.opts['hash_type'] + ) + except Exception: + jid = '' pub_data = self.pub( tgt, fun, @@ -288,6 +288,11 @@ class LocalClient(object): ret, jid=jid, timeout=timeout) + if not pub_data: + err = ('Failed to authenticate, is this user permitted to execute ' + 'commands?\n') + sys.stderr.write(err) + sys.exit(4) if pub_data['jid'] == '0': # Failed to connect to the master and send the pub return {} @@ -312,10 +317,13 @@ class LocalClient(object): arg = condition_kwarg(arg, kwarg) if timeout is None: timeout = self.opts['timeout'] - jid = salt.utils.prep_jid( - self.opts['cachedir'], - self.opts['hash_type'] - ) + try: + jid = salt.utils.prep_jid( + self.opts['cachedir'], + self.opts['hash_type'] + ) + except Exception: + jid = '' pub_data = self.pub( tgt, fun, @@ -324,6 +332,11 @@ class LocalClient(object): ret, jid=jid, timeout=timeout) + if not pub_data: + err = ('Failed to authenticate, is this user permitted to execute ' + 'commands?\n') + sys.stderr.write(err) + sys.exit(4) if pub_data['jid'] == '0': print('Failed to connect to the Master, is the Salt Master running?') yield {} @@ -369,6 +382,11 @@ class LocalClient(object): ret, jid=jid, timeout=timeout) + if not pub_data: + err = ('Failed to authenticate, is this user permitted to execute ' + 'commands?\n') + sys.stderr.write(err) + sys.exit(4) if pub_data['jid'] == '0': # Failed to connect to the master and send the pub yield {} @@ -409,6 +427,11 @@ class LocalClient(object): ret, jid=jid, timeout=timeout) + if not pub_data: + err = ('Failed to authenticate, is this user permitted to execute ' + 'commands?\n') + sys.stderr.write(err) + sys.exit(4) if pub_data['jid'] == '0': # Failed to connect to the master and send the pub yield {} @@ -448,6 +471,11 @@ class LocalClient(object): ret, jid=jid, timeout=timeout) + if not pub_data: + err = ('Failed to authenticate, is this user permitted to execute ' + 'commands?\n') + sys.stderr.write(err) + sys.exit(4) if pub_data['jid'] == '0': # Failed to connect to the master and send the pub return {} @@ -524,7 +552,7 @@ class LocalClient(object): # The timeout +1 has not been reached and there is still a # write tag for the syndic continue - if len(fret) >= len(minions): + if len(found.intersection(minions)) >= len(minions): # All minions have returned, break out of the loop break if int(time.time()) > start + timeout: @@ -542,7 +570,7 @@ class LocalClient(object): continue if verbose: if tgt_type == 'glob' or tgt_type == 'pcre': - if not len(fret) >= len(minions): + if len(found.intersection(minions)) >= len(minions): print('\nThe following minions did not return:') fail = sorted(list(minions.difference(found))) for minion in fail: @@ -596,7 +624,7 @@ class LocalClient(object): # The timeout +1 has not been reached and there is still a # write tag for the syndic continue - if len(ret) >= len(minions): + if len(found.intersection(minions)) >= len(minions): break if int(time.time()) > start + timeout: break @@ -649,7 +677,7 @@ class LocalClient(object): # The timeout +1 has not been reached and there is still a # write tag for the syndic continue - if len(ret) >= len(minions): + if len(set(ret.keys()).intersection(minions)) >= len(minions): # All Minions have returned return ret if int(time.time()) > start + timeout: @@ -704,7 +732,7 @@ class LocalClient(object): # The timeout +1 has not been reached and there is still a # write tag for the syndic continue - if len(ret) >= len(minions): + if len(set(ret.keys()).intersection(minions)) >= len(minions): return ret if int(time.time()) > start + timeout: return ret @@ -731,7 +759,6 @@ class LocalClient(object): print('-' * len(msg) + '\n') if timeout is None: timeout = self.opts['timeout'] - inc_timeout = timeout jid_dir = salt.utils.jid_dir( jid, self.opts['cachedir'], @@ -743,7 +770,7 @@ class LocalClient(object): wtag = os.path.join(jid_dir, 'wtag*') # Check to see if the jid is real, if not return the empty dict if not os.path.isdir(jid_dir): - return ret_ + return ret # Wait for the hosts to check in while True: raw = self.event.get_event(timeout, jid) @@ -752,12 +779,12 @@ class LocalClient(object): ret[raw['id']] = {'ret': raw['return']} if 'out' in raw: ret[raw['id']]['out'] = raw['out'] - if len(found) >= len(minions): + if len(found.intersection(minions)) >= len(minions): # All minions have returned, break out of the loop break continue # Then event system timeout was reached and nothing was returned - if len(found) >= len(minions): + if len(found.intersection(minions)) >= len(minions): # All minions have returned, break out of the loop break if glob.glob(wtag) and not int(time.time()) > start + timeout + 1: @@ -815,17 +842,20 @@ class LocalClient(object): while True: raw = self.event.get_event(timeout, jid) if not raw is None: + if 'syndic' in raw: + minions.update(raw['syndic']) + continue found.add(raw['id']) ret = {raw['id']: {'ret': raw['return']}} if 'out' in raw: ret[raw['id']]['out'] = raw['out'] yield ret - if len(found) >= len(minions): + if len(found.intersection(minions)) >= len(minions): # All minions have returned, break out of the loop break continue # Then event system timeout was reached and nothing was returned - if len(found) >= len(minions): + if len(found.intersection(minions)) >= len(minions): # All minions have returned, break out of the loop break if glob.glob(wtag) and not int(time.time()) > start + timeout + 1: @@ -867,8 +897,6 @@ class LocalClient(object): self.opts['cachedir'], self.opts['hash_type'] ) - start = 999999999999 - gstart = int(time.time()) found = set() # Check to see if the jid is real, if not return the empty dict if not os.path.isdir(jid_dir): @@ -926,15 +954,19 @@ class LocalClient(object): match the regex, this will then be used to parse the returns to make sure everyone has checked back in. ''' - return {'glob': self._check_glob_minions, - 'pcre': self._check_pcre_minions, - 'list': self._check_list_minions, - 'grain': self._check_grain_minions, - 'grain_pcre': self._check_grain_pcre_minions, - 'exsel': self._all_minions, - 'pillar': self._all_minions, - 'compound': self._all_minions, - }[expr_form](expr) + try: + minions = {'glob': self._check_glob_minions, + 'pcre': self._check_pcre_minions, + 'list': self._check_list_minions, + 'grain': self._check_grain_minions, + 'grain_pcre': self._check_grain_pcre_minions, + 'exsel': self._all_minions, + 'pillar': self._all_minions, + 'compound': self._all_minions, + }[expr_form](expr) + except Exception: + minions = expr + return minions def pub(self, tgt, fun, arg=(), expr_form='glob', ret='', jid='', timeout=5): @@ -991,13 +1023,7 @@ class LocalClient(object): # return what we get back minions = self.check_minions(tgt, expr_form) - if self.opts['order_masters']: - # If we're a master of masters, ignore the check_minion and - # set the minions to the target. This speeds up wait time - # for lists and ranges and makes regex and other expression - # forms possible - minions = tgt - elif not minions: + if not minions: return {'jid': None, 'minions': minions} @@ -1023,6 +1049,8 @@ class LocalClient(object): 'tcp://{0[interface]}:{0[ret_port]}'.format(self.opts), ) payload = sreq.send('clear', payload_kwargs) + if not payload: + return payload return {'jid': payload['load']['jid'], 'minions': minions} diff --git a/salt/config.py b/salt/config.py index dbd13ecd52..7864189169 100644 --- a/salt/config.py +++ b/salt/config.py @@ -7,7 +7,6 @@ import glob import os import socket import logging -import tempfile # import third party libs import yaml @@ -26,10 +25,11 @@ from salt.exceptions import SaltClientError log = logging.getLogger(__name__) -__dflt_log_datefmt = '%H:%M:%S' +__dflt_log_datefmt = '%Y-%m-%d %H:%M:%S' __dflt_log_fmt_console = '[%(levelname)-8s] %(message)s' __dflt_log_fmt_logfile = '%(asctime)s,%(msecs)03.0f [%(name)-17s][%(levelname)-8s] %(message)s' + def _validate_file_roots(file_roots): ''' If the file_roots option has a key that is None then we will error out, @@ -139,8 +139,9 @@ def prepend_root_dir(opts, path_options): root_dir = os.path.abspath(opts['root_dir']) for path_option in path_options: if path_option in opts: - opts[path_option] = os.path.normpath( - os.sep.join([root_dir, opts[path_option]])) + if opts[path_option].startswith(opts['root_dir']): + opts[path_option] = opts[path_option][len(opts['root_dir']):] + opts[path_option] = salt.utils.path_join(root_dir, opts[path_option]) def minion_config(path): @@ -149,6 +150,7 @@ def minion_config(path): ''' opts = {'master': 'salt', 'master_port': '4506', + 'master_finger': '', 'user': 'root', 'root_dir': '/', 'pki_dir': '/etc/salt/pki', @@ -156,12 +158,16 @@ def minion_config(path): 'cachedir': '/var/cache/salt', 'cache_jobs': False, 'conf_file': path, - 'sock_dir': os.path.join(tempfile.gettempdir(), '.salt-unix'), + 'sock_dir': '/var/run/salt', + 'backup_mode': '', 'renderer': 'yaml_jinja', 'failhard': False, 'autoload_dynamic_modules': True, 'environment': None, 'state_top': 'top.sls', + 'startup_states': '', + 'sls_list': [], + 'top_file': '', 'file_client': 'remote', 'file_roots': { 'base': ['/srv/salt'], @@ -182,8 +188,11 @@ def minion_config(path): 'open_mode': False, 'multiprocessing': True, 'sub_timeout': 60, + 'ipc_mode': 'ipc', + 'tcp_pub_port': 4510, + 'tcp_pull_port': 4511, 'log_file': '/var/log/salt/minion', - 'log_level': 'warning', + 'log_level': None, 'log_level_logfile': None, 'log_datefmt': __dflt_log_datefmt, 'log_fmt_console': __dflt_log_fmt_console, @@ -191,14 +200,21 @@ def minion_config(path): 'log_granular_levels': {}, 'test': False, 'cython_enable': False, - 'state_verbose': False, + 'state_verbose': True, + 'state_output': 'full', 'acceptance_wait_time': 10, 'dns_check': True, + 'verify_env': True, 'grains': {}, 'permissive_pki_access': False, 'default_include': 'minion.d/*.conf', + 'update_url': False, + 'update_restart_services': [], } + if len(opts['sock_dir']) > len(opts['cachedir']) + 10: + opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix') + load_config(opts, path, 'SALT_MINION_CONFIG') default_include = opts.get('default_include', []) @@ -242,7 +258,7 @@ def master_config(path): 'publish_port': '4505', 'user': 'root', 'worker_threads': 5, - 'sock_dir': os.path.join(tempfile.gettempdir(), '.salt-unix'), + 'sock_dir': '/var/run/salt', 'ret_port': '4506', 'timeout': 5, 'keep_jobs': 24, @@ -258,9 +274,15 @@ def master_config(path): 'pillar_roots': { 'base': ['/srv/pillar'], }, + 'ext_pillar': {}, + 'syndic_master': '', + 'runner_dirs': [], + 'client_acl': {}, 'file_buffer_size': 1048576, + 'max_open_files': 100000, 'hash_type': 'md5', 'conf_file': path, + 'pub_refresh': True, 'open_mode': False, 'auto_accept': False, 'renderer': 'yaml_jinja', @@ -271,7 +293,7 @@ def master_config(path): 'job_cache': True, 'minion_data_cache': True, 'log_file': '/var/log/salt/master', - 'log_level': 'warning', + 'log_level': None, 'log_level_logfile': None, 'log_datefmt': __dflt_log_datefmt, 'log_fmt_console': __dflt_log_fmt_console, @@ -282,13 +304,19 @@ def master_config(path): 'cluster_mode': 'paranoid', 'range_server': 'range:80', 'serial': 'msgpack', + 'state_verbose': True, + 'state_output': 'full', 'nodegroups': {}, 'cython_enable': False, 'key_logfile': '/var/log/salt/key', + 'verify_env': True, 'permissive_pki_access': False, 'default_include': 'master.d/*.conf', } + if len(opts['sock_dir']) > len(opts['cachedir']) + 10: + opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix') + load_config(opts, path, 'SALT_MASTER_CONFIG') default_include = opts.get('default_include', []) diff --git a/salt/crypt.py b/salt/crypt.py index fedcb7f876..a30158bb4e 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -8,7 +8,6 @@ authenticating peers import os import sys import hmac -import getpass import hashlib import logging import tempfile @@ -17,9 +16,6 @@ import tempfile from M2Crypto import RSA from Crypto.Cipher import AES -# Import zeromq libs -import zmq - # Import salt utils import salt.utils import salt.payload @@ -69,7 +65,7 @@ def gen_keys(keydir, keyname, keysize): priv = '{0}.pem'.format(base) pub = '{0}.pub'.format(base) - gen = RSA.gen_key(keysize, 1) + gen = RSA.gen_key(keysize, 1, callback=lambda x,y,z:None) cumask = os.umask(191) gen.save_key(priv, None) os.umask(cumask) @@ -240,6 +236,7 @@ class Auth(object): and the decrypted aes key for transport decryption. ''' auth = {} + m_pub_fn = os.path.join(self.opts['pki_dir'], self.mpub) try: self.opts['master_ip'] = salt.utils.dns_check( self.opts['master'], @@ -247,7 +244,10 @@ class Auth(object): ) except SaltClientError: return 'retry' - sreq = salt.payload.SREQ(self.opts['master_uri']) + sreq = salt.payload.SREQ( + self.opts['master_uri'], + self.opts.get('id', '') + ) try: payload = sreq.send_auto(self.minion_sign_in_payload()) except SaltReqTimeoutError: @@ -266,21 +266,35 @@ class Auth(object): else: log.error( 'The Salt Master has cached the public key for this ' - 'node, this salt minion will wait for %s seconds ' - 'before attempting to re-authenticate', - self.opts['acceptance_wait_time'] + 'node, this salt minion will wait for {0} seconds ' + 'before attempting to re-authenticate'.format( + self.opts['acceptance_wait_time'] + ) ) return 'retry' if not self.verify_master(payload['pub_key'], payload['token']): - m_pub_fn = os.path.join(self.opts['pki_dir'], self.mpub) log.critical( 'The Salt Master server\'s public key did not authenticate!\n' 'If you are confident that you are connecting to a valid Salt ' 'Master, then remove the master public key and restart the ' - 'Salt Minion.\nThe master public key can be found at:\n%s', - m_pub_fn + 'Salt Minion.\nThe master public key can be found ' + 'at:\n{0}'.format(m_pub_fn) ) sys.exit(42) + if self.opts.get('master_finger', False): + if salt.utils.pem_finger(m_pub_fn) != self.opts['master_finger']: + log.critical(( + 'The specified fingerprint in the master configuration ' + 'file:\n{0}\nDoes not match the authenticating master\'s ' + 'key:\n{1}\nVerify that the configured fingerprint ' + 'matches the fingerprint of the correct master and that ' + 'this minion is not subject to a man in the middle attack' + ).format( + self.opts['master_finger'], + salt.utils.pem_finger(m_pub_fn) + ) + ) + sys.exit(42) auth['aes'] = self.decrypt_aes(payload['aes']) auth['publish_port'] = payload['publish_port'] return auth @@ -334,7 +348,14 @@ class Crypticle(object): aes_key, hmac_key = self.keys sig = data[-self.SIG_SIZE:] data = data[:-self.SIG_SIZE] - if hmac.new(hmac_key, data, hashlib.sha256).digest() != sig: + mac_bytes = hmac.new(hmac_key, data, hashlib.sha256).digest() + if len(mac_bytes) != len(sig): + log.warning('Failed to authenticate message') + raise AuthenticationError('message authentication failed') + result = 0 + for x, y in zip(mac_bytes, sig): + result |= ord(x) ^ ord(y) + if result != 0: log.warning('Failed to authenticate message') raise AuthenticationError('message authentication failed') iv_bytes = data[:self.AES_BLOCK_SIZE] diff --git a/salt/fileclient.py b/salt/fileclient.py index 9a7e5c798e..42b49795ca 100644 --- a/salt/fileclient.py +++ b/salt/fileclient.py @@ -12,7 +12,6 @@ import subprocess # Import third-party libs import yaml -import zmq # Import salt libs from salt.exceptions import MinionError, SaltReqTimeoutError @@ -21,6 +20,7 @@ import salt.crypt import salt.loader import salt.utils import salt.payload +import salt.utils import salt.utils.templates from salt._compat import ( URLError, HTTPError, BaseHTTPServer, urlparse, url_open) @@ -69,7 +69,7 @@ class Client(object): filelist = [] - for root, dirs, files in os.walk(destdir): + for root, dirs, files in os.walk(destdir, followlinks=True): for name in files: path = os.path.join(root, name) filelist.append(path) @@ -96,7 +96,8 @@ class Client(object): def get_file(self, path, dest='', makedirs=False, env='base'): ''' - Copies a file from the local files or master depending on implementation + Copies a file from the local files or master depending on + implementation ''' raise NotImplementedError @@ -138,6 +139,11 @@ class Client(object): ''' ret = [] path = self._check_proto(path) + log.info( + 'Caching directory \'{0}\' for environment \'{1}\''.format( + path, env + ) + ) for fn_ in self.file_list(env): if fn_.startswith(path): local = self.cache_file('salt://{0}'.format(fn_), env) @@ -148,20 +154,20 @@ class Client(object): if include_empty: # Break up the path into a list containing the bottom-level directory # (the one being recursively copied) and the directories preceding it - separated = string.rsplit(path,'/',1) - if len(separated) != 2: - # No slashes in path. (This means all files in env will be copied) - prefix = '' - else: - prefix = separated[0] + #separated = string.rsplit(path, '/', 1) + #if len(separated) != 2: + # # No slashes in path. (This means all files in env will be copied) + # prefix = '' + #else: + # prefix = separated[0] for fn_ in self.file_list_emptydirs(env): if fn_.startswith(path): - dest = os.path.normpath( - os.sep.join([ - self.opts['cachedir'], - 'files', - env])) - minion_dir = '%s/%s' % (dest,fn_) + dest = salt.utils.path_join( + self.opts['cachedir'], + 'files', + env + ) + minion_dir = '{0}/{1}'.format(dest, fn_) if not os.path.isdir(minion_dir): os.makedirs(minion_dir) ret.append(minion_dir) @@ -197,6 +203,12 @@ class Client(object): ''' return [] + def dir_list(self, env='base'): + ''' + This function must be overwritten + ''' + return [] + def is_cached(self, path, env='base'): ''' Returns the full path to a file if it is cached locally on the minion @@ -224,9 +236,9 @@ class Client(object): if path.endswith('.sls'): # is an sls module! if path.endswith('{0}init.sls'.format(os.sep)): - states.append(path.replace(os.sep, '.')[:-9]) + states.append(path.replace('/', '.')[:-9]) else: - states.append(path.replace(os.sep, '.')[:-4]) + states.append(path.replace('/', '.')[:-4]) return states def get_state(self, sls, env): @@ -266,16 +278,20 @@ class Client(object): # Remove the leading directories from path to derive # the relative path on the minion. minion_relpath = string.lstrip(fn_[len(prefix):], '/') - ret.append(self.get_file('salt://{0}'.format(fn_), - '%s/%s' % (dest, minion_relpath), - True, env)) + ret.append( + self.get_file( + 'salt://{0}'.format(fn_), + '{0}/{1}'.format(dest, minion_relpath), + True, env + ) + ) # Replicate empty dirs from master for fn_ in self.file_list_emptydirs(env): if fn_.startswith(path): # Remove the leading directories from path to derive # the relative path on the minion. minion_relpath = string.lstrip(fn_[len(prefix):], '/') - minion_mkdir = '%s/%s' % (dest, minion_relpath) + minion_mkdir = '{0}/{1}'.format(dest, minion_relpath) os.makedirs(minion_mkdir) ret.append(minion_mkdir) ret.sort() @@ -296,13 +312,13 @@ class Client(object): else: return '' else: - dest = os.path.normpath( - os.sep.join([ - self.opts['cachedir'], - 'extrn_files', - env, - url_data.netloc, - url_data.path])) + dest = salt.utils.path_join( + self.opts['cachedir'], + 'extrn_files', + env, + url_data.netloc, + url_data.path + ) destdir = os.path.dirname(dest) if not os.path.isdir(destdir): os.makedirs(destdir) @@ -354,13 +370,13 @@ class Client(object): return '' if not dest: # No destination passed, set the dest as an extrn_files cache - dest = os.path.normpath( - os.sep.join([ - self.opts['cachedir'], - 'extrn_files', - env, - url_data.netloc, - url_data.path])) + dest = salt.utils.path_join( + self.opts['cachedir'], + 'extrn_files', + env, + url_data.netloc, + url_data.path + ) destdir = os.path.dirname(dest) if not os.path.isdir(destdir): if makedirs: @@ -413,7 +429,7 @@ class LocalClient(Client): if env not in self.opts['file_roots']: return ret for path in self.opts['file_roots'][env]: - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(path, followlinks=True): for fn in files: ret.append( os.path.relpath( @@ -434,11 +450,23 @@ class LocalClient(Client): if env not in self.opts['file_roots']: return ret for path in self.opts['file_roots'][env]: - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(path, followlinks=True): if len(dirs) == 0 and len(files) == 0: ret.append(os.path.relpath(root, path)) return ret + def dir_list(self, env='base'): + ''' + List the dirs in the file_roots + ''' + ret = [] + if env not in self.opts['file_roots']: + return ret + for path in self.opts['file_roots'][env]: + for root, dirs, files in os.walk(path, followlinks=True): + ret.append(os.path.relpath(root, path)) + return ret + def hash_file(self, path, env='base'): ''' Return the hash of a file, to get the hash of a file in the file_roots @@ -450,9 +478,8 @@ class LocalClient(Client): path = self._check_proto(path) except MinionError: if not os.path.isfile(path): - err = ('Specified file {0} is not present to generate ' - 'hash').format(path) - log.warning(err) + err = 'Specified file {0} is not present to generate hash' + log.warning(err.format(path)) return ret else: with open(path, 'rb') as f: @@ -532,6 +559,7 @@ class RemoteClient(Client): dest is ommited, then the downloaded file will be placed in the minion cache ''' + log.info('Fetching file \'{0}\''.format(path)) path = self._check_proto(path) load = {'path': path, 'env': env, @@ -612,6 +640,23 @@ class RemoteClient(Client): except SaltReqTimeoutError: return '' + def dir_list(self, env='base'): + ''' + List the dirs on the master + ''' + load = {'env': env, + 'cmd': '_dir_list'} + try: + return self.auth.crypticle.loads( + self.sreq.send( + 'aes', + self.auth.crypticle.dumps(load), + 3, + 60) + ) + except SaltReqTimeoutError: + return '' + def hash_file(self, path, env='base'): ''' Return the hash of a file, to get the hash of a file on the salt @@ -622,9 +667,8 @@ class RemoteClient(Client): path = self._check_proto(path) except MinionError: if not os.path.isfile(path): - err = ('Specified file {0} is not present to generate ' - 'hash').format(path) - log.warning(err) + err = 'Specified file {0} is not present to generate hash' + log.warning(err.format(path)) return {} else: ret = {} diff --git a/salt/grains/core.py b/salt/grains/core.py index bebe68872f..69136c4d82 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -18,6 +18,13 @@ import socket import sys import re import platform + +# Extend the default list of supported distros. This will be used for the +# /etc/DISTRO-release checking that is part of platform.linux_distribution() +from platform import _supported_dists +_supported_dists += ('arch', 'mageia', 'meego', 'vmware', 'bluewhite64', + 'slamd64', 'enterprise', 'ovs', 'system') + import salt.utils # Solve the Chicken and egg problem where grains need to run before any @@ -26,38 +33,21 @@ import salt.modules.cmdmod __salt__ = {'cmd.run': salt.modules.cmdmod._run_quiet} -def _kernel(): - ''' - Return the kernel type - ''' - # Provides: - # kernel - grains = {} - grains['kernel'] = __salt__['cmd.run']('uname -s').strip() - - if grains['kernel'] == 'aix': - grains['kernelrelease'] = __salt__['cmd.run']('oslevel -s').strip() - else: - grains['kernelrelease'] = __salt__['cmd.run']('uname -r').strip() - if 'kernel' not in grains: - grains['kernel'] = 'Unknown' - if not grains['kernel']: - grains['kernel'] = 'Unknown' - return grains - - def _windows_cpudata(): ''' Return the cpu information for Windows systems architecture ''' # Provides: # cpuarch - # num_cpus # cpu_model grains = {} - grains['cpuarch'] = platform.machine() if 'NUMBER_OF_PROCESSORS' in os.environ: - grains['num_cpus'] = os.environ['NUMBER_OF_PROCESSORS'] + # Cast to int so that the logic isn't broken when used as a + # conditional in templating. Also follows _linux_cpudata() + try: + grains['num_cpus'] = int(os.environ['NUMBER_OF_PROCESSORS']) + except ValueError: + grains['num_cpus'] = 1 grains['cpu_model'] = platform.processor() return grains @@ -67,25 +57,11 @@ def _linux_cpudata(): Return the cpu information for Linux systems architecture ''' # Provides: - # cpuarch # num_cpus # cpu_model # cpu_flags grains = {} cpuinfo = '/proc/cpuinfo' - # Grab the Arch - arch = __salt__['cmd.run']('uname -m').strip() - grains['cpuarch'] = arch - # Some systems such as Debian don't like uname -m - # so fallback gracefully to the processor type - if not grains['cpuarch'] or grains['cpuarch'] == 'unknown': - arch = __salt__['cmd.run']('uname -p') - grains['cpuarch'] = arch - if not grains['cpuarch'] or grains['cpuarch'] == 'unknown': - arch = __salt__['cmd.run']('uname -i') - grains['cpuarch'] = arch - if not grains['cpuarch'] or grains['cpuarch'] == 'unknown': - grains['cpuarch'] = 'Unknown' # Parse over the cpuinfo file if os.path.isfile(cpuinfo): for line in open(cpuinfo, 'r').readlines(): @@ -126,12 +102,30 @@ def _bsd_cpudata(osdata): grains['cpu_flags'] = [] try: grains['num_cpus'] = int(grains['num_cpus']) - except Exception: - grains['num_cpus'] = 0 + except ValueError: + grains['num_cpus'] = 1 return grains +def _sunos_cpudata(osdata): + ''' + Return the cpu information for Solaris-like systems + ''' + # Provides: + # cpuarch + # num_cpus + # cpu_model + grains = {'num_cpus': 0} + + grains['cpuarch'] = __salt__['cmd.run']('uname -p').strip() + for line in __salt__['cmd.run']('/usr/sbin/psrinfo 2>/dev/null').split('\n'): + grains['num_cpus'] += 1 + grains['cpu_model'] = __salt__['cmd.run']('kstat -p cpu_info:0:cpu_info0:implementation').split()[1].strip() + + return grains + + def _memdata(osdata): ''' Gather information about the system memory @@ -154,6 +148,11 @@ def _memdata(osdata): if sysctl: mem = __salt__['cmd.run']('{0} -n hw.physmem'.format(sysctl)).strip() grains['mem_total'] = str(int(mem) / 1024 / 1024) + elif osdata['kernel'] == 'SunOS': + for line in __salt__['cmd.run']('/usr/sbin/prtconf 2>/dev/null').split('\n'): + comps = line.split(' ') + if comps[0].strip() == 'Memory' and comps[1].strip() == 'size:': + grains['mem_total'] = int(comps[2].strip()) elif osdata['kernel'] == 'Windows': for line in __salt__['cmd.run']('SYSTEMINFO /FO LIST').split('\n'): comps = line.split(':') @@ -195,6 +194,9 @@ def _virtual(osdata): # Product Name: Virtual Machine elif 'Manufacturer: Microsoft' in output and 'Virtual Machine' in output: grains['virtual'] = 'VirtualPC' + # Manufacturer: Parallels Software International Inc. + elif 'Parallels Software' in output: + grains['virtual'] = 'Parallels' # Fall back to lspci if dmidecode isn't available elif lspci: model = __salt__['cmd.run']('lspci').lower() @@ -207,7 +209,7 @@ def _virtual(osdata): grains['virtual'] = 'kvm' elif 'virtio' in model: grains['virtual'] = 'kvm' - choices = ('Linux', 'OpenBSD', 'SunOS', 'HP-UX') + choices = ('Linux', 'OpenBSD', 'HP-UX') isdir = os.path.isdir if osdata['kernel'] in choices: if isdir('/proc/vz'): @@ -245,9 +247,7 @@ def _virtual(osdata): # If a Dom0 or DomU was detected, obviously this is xen if 'dom' in grains.get('virtual_subtype', '').lower(): grains['virtual'] = 'xen' - elif isdir('/.SUNWnative'): - grains['virtual'] = 'zone' - elif os.path.isfile('/proc/cpuinfo'): + if os.path.isfile('/proc/cpuinfo'): if 'QEMU Virtual CPU' in open('/proc/cpuinfo', 'r').read(): grains['virtual'] = 'kvm' elif osdata['kernel'] == 'FreeBSD': @@ -264,6 +264,16 @@ def _virtual(osdata): grains['virtual_subtype'] = 'jail' if 'QEMU Virtual CPU' in model: grains['virtual'] = 'kvm' + elif osdata['kernel'] == 'SunOS': + # Check if it's a "regular" zone. (i.e. Solaris 10/11 zone) + zonename = salt.utils.which('zonename') + if zonename: + zone = __salt__['cmd.run']('{0}'.format(zonename)).strip() + if zone != "global": + grains['virtual'] = 'zone' + # Check if it's a branded zone (i.e. Solaris 8/9 zone) + if isdir('/.SUNWnative'): + grains['virtual'] = 'zone' return grains @@ -275,6 +285,8 @@ def _ps(osdata): bsd_choices = ('FreeBSD', 'NetBSD', 'OpenBSD', 'MacOS') if osdata['os'] in bsd_choices: grains['ps'] = 'ps auxwww' + if osdata['os'] == 'Solaris': + grains['ps'] = '/usr/ucb/ps auxwww' elif osdata['os'] == 'Windows': grains['ps'] = 'tasklist.exe' elif osdata.get('virtual', '') == 'openvzhn': @@ -283,33 +295,11 @@ def _ps(osdata): grains['ps'] = 'ps -efH' return grains - -def _linux_platform_data(osdata): - ''' - The platform module is very smart about figuring out linux distro - information. Instead of re-inventing the wheel, lets use it! - ''' - # Provides: - # osrelease - # oscodename - grains = {} - (osname, osrelease, oscodename) = platform.dist() - if 'os' not in osdata and osname: - grains['os'] = osname - if osrelease: - grains['osrelease'] = osrelease - if oscodename: - grains['oscodename'] = oscodename - return grains - - def _windows_platform_data(osdata): ''' Use the platform module for as much as we can. ''' # Provides: - # osrelease - # osversion # osmanufacturer # manufacturer # productname @@ -320,13 +310,6 @@ def _windows_platform_data(osdata): # windowsdomain grains = {} - (osname, hostname, osrelease, osversion, machine, processor) = platform.uname() - if 'os' not in osdata and osname: - grains['os'] = osname - if osrelease: - grains['osrelease'] = osrelease - if osversion: - grains['osversion'] = osversion get_these_grains = { 'OS Manufacturer': 'osmanufacturer', 'System Manufacturer': 'manufacturer', @@ -355,143 +338,118 @@ def id_(): ''' return {'id': __opts__['id']} +# This maps (at most) the first ten characters (no spaces, lowercased) of +# 'osfullname' to the 'os' grain that Salt traditionally uses. +_os_name_map = { + 'redhatente': 'RedHat', + 'debian': 'Debian', + 'arch': 'Arch', +} + +# Map the 'os' grain to the 'os_family' grain +_os_family_map = { + 'Ubuntu': 'Debian', + 'Fedora': 'RedHat', + 'CentOS': 'RedHat', + 'GoOSe': 'RedHat', + 'Scientific': 'RedHat', + 'Amazon': 'RedHat', + 'CloudLinux': 'RedHat', + 'Mandrake': 'Mandriva', + 'ESXi': 'VMWare', + 'VMWareESX': 'VMWare', + 'Bluewhite64': 'Bluewhite', + 'Slamd64': 'Slackware', + 'OVS': 'Oracle', + 'OEL': 'Oracle', + 'SLES': 'Suse', + 'SLED': 'Suse', + 'openSUSE': 'Suse', + 'SUSE': 'Suse' +} + def os_data(): ''' Return grains pertaining to the operating system ''' grains = {} - if 'os' in os.environ: - if os.environ['os'].startswith('Windows'): - grains['os'] = 'Windows' - grains['os_family'] = 'Windows' - grains['kernel'] = 'Windows' - grains.update(_memdata(grains)) - grains.update(_windows_platform_data(grains)) - grains.update(_windows_cpudata()) - grains.update(_ps(grains)) - return grains - grains.update(_kernel()) - - if grains['kernel'] == 'Linux': + # Windows Server 2008 64-bit + # ('Windows', 'MINIONNAME', '2008ServerR2', '6.1.7601', 'AMD64', 'Intel64 Fam ily 6 Model 23 Stepping 6, GenuineIntel') + # Ubuntu 10.04 + # ('Linux', 'FIRE66VMA01', '2.6.32-38-server', '#83-Ubuntu SMP Wed Jan 4 11:26:59 UTC 2012', 'x86_64', '') + (grains['kernel'], grains['host'], + grains['kernelrelease'], version, grains['cpuarch'], _) = platform.uname() + if grains['kernel'] == 'Windows': + grains['osrelease'] = grains['kernelrelease'] + grains['osversion'] = grains['kernelrelease'] = version + grains['os'] = 'Windows' + grains['os_family'] = 'Windows' + grains.update(_memdata(grains)) + grains.update(_windows_platform_data(grains)) + grains.update(_windows_cpudata()) + grains.update(_ps(grains)) + return grains + elif grains['kernel'] == 'Linux': # Add lsb grains on any distro with lsb-release - if os.path.isfile('/etc/lsb-release'): - for line in open('/etc/lsb-release').readlines(): - # Matches any possible format: - # DISTRIB_ID="Ubuntu" - # DISTRIB_ID='Mageia' - # DISTRIB_ID=Fedora - # DISTRIB_RELEASE='10.10' - # DISTRIB_CODENAME='squeeze' - # DISTRIB_DESCRIPTION='Ubuntu 10.10' - regex = re.compile('^(DISTRIB_(?:ID|RELEASE|CODENAME|DESCRIPTION))=(?:\'|")?([\w\s\.-_]+)(?:\'|")?') - match = regex.match(line) - if match: - # Adds: lsb_distrib_{id,release,codename,description} - grains['lsb_{0}'.format(match.groups()[0].lower())] = match.groups()[1].rstrip() try: import lsb_release release = lsb_release.get_distro_information() for key, value in release.iteritems(): grains['lsb_{0}'.format(key.lower())] = value # override /etc/lsb-release except ImportError: - pass - if os.path.isfile('/etc/arch-release'): - grains['os'] = 'Arch' - grains['os_family'] = 'Arch' - elif os.path.isfile('/etc/debian_version'): - grains['os'] = 'Debian' - grains['os_family'] = 'Debian' - if 'lsb_distrib_id' in grains: - if 'Ubuntu' in grains['lsb_distrib_id']: - grains['os'] = 'Ubuntu' - elif os.path.isfile('/etc/issue.net') and \ - 'Ubuntu' in open('/etc/issue.net').readline(): - grains['os'] = 'Ubuntu' - elif os.path.isfile('/etc/gentoo-release'): - grains['os'] = 'Gentoo' - grains['os_family'] = 'Gentoo' - elif os.path.isfile('/etc/fedora-release'): - grains['os'] = 'Fedora' - grains['os_family'] = 'RedHat' - elif os.path.isfile('/etc/mandriva-version'): - grains['os'] = 'Mandriva' - grains['os_family'] = 'Mandriva' - elif os.path.isfile('/etc/mandrake-version'): - grains['os'] = 'Mandrake' - grains['os_family'] = 'Mandriva' - elif os.path.isfile('/etc/mageia-version'): - grains['os'] = 'Mageia' - grains['os_family'] = 'Mageia' - elif os.path.isfile('/etc/meego-version'): - grains['os'] = 'MeeGo' - grains['os_family'] = 'MeeGo' - elif os.path.isfile('/etc/vmware-version'): - grains['os'] = 'VMWareESX' - grains['os_family'] = 'VMWare' - elif os.path.isfile('/etc/bluewhite64-version'): - grains['os'] = 'Bluewhite64' - grains['os_family'] = 'Bluewhite' - elif os.path.isfile('/etc/slamd64-version'): - grains['os'] = 'Slamd64' - grains['os_family'] = 'Slackware' - elif os.path.isfile('/etc/slackware-version'): - grains['os'] = 'Slackware' - grains['os_family'] = 'Slackware' - elif os.path.isfile('/etc/enterprise-release'): - grains['os_family'] = 'Oracle' - if os.path.isfile('/etc/ovs-release'): - grains['os'] = 'OVS' - else: - grains['os'] = 'OEL' - elif os.path.isfile('/etc/redhat-release'): - grains['os_family'] = 'RedHat' - data = open('/etc/redhat-release', 'r').read() - if 'centos' in data.lower(): - grains['os'] = 'CentOS' - elif 'scientific' in data.lower(): - grains['os'] = 'Scientific' - else: - grains['os'] = 'RedHat' - elif os.path.isfile('/etc/system-release'): - grains['os_family'] = 'RedHat' - data = open('/etc/system-release', 'r').read() - if 'amazon' in data.lower(): - grains['os'] = 'Amazon' - elif os.path.isfile('/etc/SuSE-release'): - grains['os_family'] = 'Suse' - data = open('/etc/SuSE-release', 'r').read() - if 'SUSE LINUX Enterprise Server' in data: - grains['os'] = 'SLES' - elif 'SUSE LINUX Enterprise Desktop' in data: - grains['os'] = 'SLED' - elif 'openSUSE' in data: - grains['os'] = 'openSUSE' - else: - grains['os'] = 'SUSE' + # if the python library isn't available, default to regex + if os.path.isfile('/etc/lsb-release'): + for line in open('/etc/lsb-release').readlines(): + # Matches any possible format: + # DISTRIB_ID="Ubuntu" + # DISTRIB_ID='Mageia' + # DISTRIB_ID=Fedora + # DISTRIB_RELEASE='10.10' + # DISTRIB_CODENAME='squeeze' + # DISTRIB_DESCRIPTION='Ubuntu 10.10' + regex = re.compile('^(DISTRIB_(?:ID|RELEASE|CODENAME|DESCRIPTION))=(?:\'|")?([\w\s\.-_]+)(?:\'|")?') + match = regex.match(line) + if match: + # Adds: lsb_distrib_{id,release,codename,description} + grains['lsb_{0}'.format(match.groups()[0].lower())] = match.groups()[1].rstrip() # Use the already intelligent platform module to get distro info - grains.update(_linux_platform_data(grains)) - # If the Linux version can not be determined - if not 'os' in grains: - grains['os'] = 'Unknown {0}'.format(grains['kernel']) - grains['os_family'] = 'Unknown' - elif grains['kernel'] == 'sunos': + (osname, osrelease, oscodename) = platform.linux_distribution( + supported_dists=_supported_dists) + # Try to assign these three names based on the lsb info, they tend to + # be more accurate than what python gets from /etc/DISTRO-release. + # It's worth noting that Ubuntu has patched their Python distribution + # so that platform.linux_distribution() does the /etc/lsb-release + # parsing, but we do it anyway here for the sake for full portability. + grains['osfullname'] = grains.get('lsb_distrib_id', osname) + grains['osrelease'] = grains.get('lsb_distrib_release', osrelease) + grains['oscodename'] = grains.get('lsb_distrib_codename', oscodename) + # return the first ten characters with no spaces, lowercased + shortname = grains['osfullname'].replace(' ', '').lower()[:10] + # this maps the long names from the /etc/DISTRO-release files to the + # traditional short names that Salt has used. + grains['os'] = _os_name_map.get(shortname, grains['osfullname']) + grains.update(_linux_cpudata()) + elif grains['kernel'] == 'SunOS': grains['os'] = 'Solaris' - grains['os_family'] = 'Solaris' + grains.update(_sunos_cpudata(grains)) elif grains['kernel'] == 'VMkernel': grains['os'] = 'ESXi' - grains['os_family'] = 'VMWare' elif grains['kernel'] == 'Darwin': grains['os'] = 'MacOS' - grains['os_family'] = 'MacOS' grains.update(_bsd_cpudata(grains)) else: grains['os'] = grains['kernel'] - grains['os_family'] = grains['kernel'] - if grains['kernel'] == 'Linux': - grains.update(_linux_cpudata()) - elif grains['kernel'] in ('FreeBSD', 'OpenBSD'): + if grains['kernel'] in ('FreeBSD', 'OpenBSD'): grains.update(_bsd_cpudata(grains)) + if not grains['os']: + grains['os'] = 'Unknown {0}'.format(grains['kernel']) + grains['os_family'] = 'Unknown' + else: + # this assigns family names based on the os name + # family defaults to the os name if not found + grains['os_family'] = _os_family_map.get(grains['os'], + grains['os']) grains.update(_memdata(grains)) @@ -516,14 +474,9 @@ def hostname(): # localhost # domain grains = {} - grains['fqdn'] = socket.getfqdn() - comps = grains['fqdn'].split('.') - grains['host'] = comps[0] grains['localhost'] = socket.gethostname() - if len(comps) > 1: - grains['domain'] = '.'.join(comps[1:]) - else: - grains['domain'] = '' + grains['fqdn'] = socket.getfqdn() + (grains['host'], grains['domain']) = grains['fqdn'].partition('.')[::2] return grains diff --git a/salt/grains/opts.py b/salt/grains/opts.py new file mode 100644 index 0000000000..9dd357ec81 --- /dev/null +++ b/salt/grains/opts.py @@ -0,0 +1,12 @@ +''' +Simple grain to merge the opts into the grains directly if the grain_opts +configuration value is set +''' + +def opts(): + ''' + Return the minion configuration settings + ''' + if __opts__.get('grain_opts', False) or __pillar__.get('grain_opts', False): + return __opts__ + return {} diff --git a/salt/loader.py b/salt/loader.py index 7040e66dac..e983b4567b 100644 --- a/salt/loader.py +++ b/salt/loader.py @@ -5,19 +5,21 @@ Routines to set up a minion # This module still needs package support, so that the functions dict # returned can send back functions like: foo.bar.baz - # Import python libs import os import imp +import sys import salt import logging import tempfile +import traceback # Import Salt libs from salt.exceptions import LoaderError log = logging.getLogger(__name__) salt_base_path = os.path.dirname(salt.__file__) +loaded_base_name = 'salt.loaded' def _create_loader(opts, ext_type, tag, ext_dirs=True, ext_type_dirs=None): @@ -40,6 +42,10 @@ def _create_loader(opts, ext_type, tag, ext_dirs=True, ext_type_dirs=None): ext_type_types.extend(opts[ext_type_dirs]) module_dirs = ext_type_types + [ext_types, sys_types] + _generate_module('{0}.int'.format(loaded_base_name)) + _generate_module('{0}.int.{1}'.format(loaded_base_name, tag)) + _generate_module('{0}.ext'.format(loaded_base_name)) + _generate_module('{0}.ext.{1}'.format(loaded_base_name, tag)) return Loader(module_dirs, opts, tag) @@ -52,15 +58,10 @@ def minion_mods(opts): if opts.get('providers', False): if isinstance(opts['providers'], dict): for mod, provider in opts['providers'].items(): - funcs = raw_mod(opts, - provider, - functions) + funcs = raw_mod(opts, provider, functions) if funcs: for func in funcs: - f_key = '{0}{1}'.format( - mod, - func[func.rindex('.'):] - ) + f_key = '{0}{1}'.format(mod, func[func.rindex('.'):]) functions[f_key] = funcs[func] return functions @@ -73,12 +74,14 @@ def raw_mod(opts, name, functions): return load.gen_module(name, functions) -def returners(opts): +def returners(opts, functions): ''' Returns the returner modules ''' load = _create_loader(opts, 'returners', 'returner') - return load.filter_func('returner') + pack = {'name': '__salt__', + 'value': functions} + return load.filter_func('returner', pack) def pillars(opts, functions): @@ -105,7 +108,9 @@ def render(opts, functions): ''' Returns the render modules ''' - load = _create_loader(opts, 'renderers', 'render', ext_type_dirs='render_dirs') + load = _create_loader( + opts, 'renderers', 'render', ext_type_dirs='render_dirs' + ) pack = {'name': '__salt__', 'value': functions} rend = load.filter_func('render', pack) @@ -125,20 +130,21 @@ def grains(opts): if not 'grains' in opts: pre_opts = {} salt.config.load_config( - pre_opts, - opts['conf_file'], - 'SALT_MINION_CONFIG' - ) + pre_opts, opts['conf_file'], 'SALT_MINION_CONFIG' + ) default_include = pre_opts.get('default_include', []) include = pre_opts.get('include', []) - pre_opts = salt.config.include_config(default_include, pre_opts, - opts['conf_file'], verbose=False) - pre_opts = salt.config.include_config(include, pre_opts, - opts['conf_file'], verbose=True) + pre_opts = salt.config.include_config( + default_include, pre_opts, opts['conf_file'], verbose=False + ) + pre_opts = salt.config.include_config( + include, pre_opts, opts['conf_file'], verbose=True + ) if 'grains' in pre_opts: opts['grains'] = pre_opts['grains'] else: opts['grains'] = {} + load = _create_loader(opts, 'grains', 'grain', ext_dirs=False) grains = load.gen_grains() grains.update(opts['grains']) @@ -151,9 +157,7 @@ def call(fun, **kwargs): ''' args = kwargs.get('args', []) dirs = kwargs.get('dirs', []) - module_dirs = [ - os.path.join(salt_base_path, 'modules'), - ] + dirs + module_dirs = [os.path.join(salt_base_path, 'modules')] + dirs load = Loader(module_dirs) return load.call(fun, args) @@ -163,14 +167,27 @@ def runner(opts): Directly call a function inside a loader directory ''' load = _create_loader( - opts, - 'runners', - 'runner', - ext_type_dirs='runner_dirs' - ) + opts, 'runners', 'runner', ext_type_dirs='runner_dirs' + ) return load.gen_functions() +def _generate_module(name): + if name in sys.modules: + return + + code = "'''Salt loaded {0} parent module'''".format(name.split('.')[-1]) + module = imp.new_module(name) + exec code in module.__dict__ + sys.modules[name] = module + + +def _mod_type(module_path): + if module_path.startswith(salt_base_path): + return 'int' + return 'ext' + + class Loader(object): ''' Used to load in arbitrary modules from a directory, the Loader can @@ -238,9 +255,9 @@ class Loader(object): return getattr( mod, fun[fun.rindex('.') + 1:])(*arg) except ImportError: - log.info("Cython is enabled in options though it's not " - "present in the system path. Skipping Cython " - "modules.") + log.info('Cython is enabled in options though it\'s not ' + 'present in the system path. Skipping Cython ' + 'modules.') return getattr(mod, fun[fun.rindex('.') + 1:])(*arg) def gen_module(self, name, functions, pack=None): @@ -264,6 +281,7 @@ class Loader(object): full = full_test if not full: return None + cython_enabled = False if self.opts.get('cython_enable', True) is True: try: @@ -274,24 +292,24 @@ class Loader(object): log.info('Cython is enabled in the options but not present ' 'in the system path. Skipping Cython modules.') try: - if full.endswith('.pyx'): + if full.endswith('.pyx') and cython_enabled: # If there's a name which ends in .pyx it means the above # cython_enabled is True. Continue... mod = pyximport.load_module(name, full, tempfile.gettempdir()) else: fn_, path, desc = imp.find_module(name, self.module_dirs) mod = imp.load_module( - '{0}_{1}'.format(name, self.tag), - fn_, - path, - desc - ) + '{0}.{1}.{2}.{3}'.format( + loaded_base_name, _mod_type(path), self.tag, name + ), fn_, path, desc + ) except ImportError as exc: - log.debug(('Failed to import module {0}: {1}').format(name, exc)) + log.debug('Failed to import module {0}: {1}'.format(name, exc)) return mod except Exception as exc: - log.warning(('Failed to import module {0}, this is due most' - ' likely to a syntax error: {1}').format(name, exc)) + trb = traceback.format_exc() + log.warning('Failed to import module {0}, this is due most likely ' + 'to a syntax error: {1}'.format(name, trb)) return mod if hasattr(mod, '__opts__'): mod.__opts__.update(self.opts) @@ -311,7 +329,7 @@ class Loader(object): if hasattr(mod, '__init__'): if callable(mod.__init__): try: - mod.__init__() + mod.__init__(self.opts) except TypeError: pass funcs = {} @@ -324,11 +342,12 @@ class Loader(object): if 'BaseException' in func.__bases__: # the callable object is an exception, don't load it continue + funcs[ - '{0}.{1}'.format( - mod.__name__[:mod.__name__.rindex('_')], - attr) - ] = func + '{0}.{1}'.format( + mod.__name__[mod.__name__.rindex('.')+1:], attr + ) + ] = func self._apply_outputter(func, mod) if not hasattr(mod, '__salt__'): mod.__salt__ = functions @@ -364,7 +383,8 @@ class Loader(object): continue if (fn_.endswith(('.py', '.pyc', '.pyo', '.so')) or (cython_enabled and fn_.endswith('.pyx')) - or os.path.isdir(fn_)): + or os.path.isdir(os.path.join(mod_dir, fn_))): + extpos = fn_.rfind('.') if extpos > 0: _name = fn_[:extpos] @@ -377,24 +397,44 @@ class Loader(object): # If there's a name which ends in .pyx it means the above # cython_enabled is True. Continue... mod = pyximport.load_module( - '{0}_{1}'.format(name, self.tag), - names[name], - tempfile.gettempdir()) + '{0}.{1}.{2}.{3}'.format( + loaded_base_name, + _mod_type(names[name]), + self.tag, + name + ), names[name], tempfile.gettempdir() + ) else: fn_, path, desc = imp.find_module(name, self.module_dirs) mod = imp.load_module( - '{0}_{1}'.format(name, self.tag), - fn_, - path, - desc - ) + '{0}.{1}.{2}.{3}'.format( + loaded_base_name, _mod_type(path), self.tag, name + ), fn_, path, desc + ) + # reload all submodules if necessary + submodules = [ + getattr(mod, sname) for sname in dir(mod) if + type(getattr(mod, sname))==type(mod) + ] + # reload only custom "sub"modules i.e is a submodule in + # parent module that are still available on disk (i.e. not + # removed during sync_modules) + for submodule in submodules: + try: + smname = '{0}.{1}.{2}'.format(loaded_base_name, self.tag, name) + smfile = os.path.splitext(submodule.__file__)[0] + ".py" + if submodule.__name__.startswith(smname) and os.path.isfile(smfile): + reload(submodule) + except AttributeError: + continue except ImportError as exc: - log.debug(('Failed to import module {0}, this is most likely' - ' NOT a problem: {1}').format(name, exc)) + log.debug('Failed to import module {0}, this is most likely ' + 'NOT a problem: {1}'.format(name, exc)) continue except Exception as exc: - log.warning(('Failed to import module {0}, this is due most' - ' likely to a syntax error: {1}').format(name, exc)) + trb = traceback.format_exc() + log.warning('Failed to import module {0}, this is due most ' + 'likely to a syntax error: {1}'.format(name, trb)) continue modules.append(mod) for mod in modules: @@ -418,7 +458,7 @@ class Loader(object): if hasattr(mod, '__init__'): if callable(mod.__init__): try: - mod.__init__() + mod.__init__(self.opts) except TypeError: pass @@ -444,10 +484,11 @@ class Loader(object): pass else: funcs[ - '{0}.{1}'.format( - mod.__name__[:mod.__name__.rindex('_')], - attr) - ] = func + '{0}.{1}'.format( + mod.__name__[mod.__name__.rindex('.')+1:], + attr + ) + ] = func self._apply_outputter(func, mod) for mod in modules: if not hasattr(mod, '__salt__'): @@ -534,10 +575,11 @@ class Loader(object): continue try: ret = fun() - except Exception as exc: + except Exception: + trb = traceback.format_exc() log.critical(('Failed to load grains defined in grain file ' - '{0} in function {1}, error: {2}').format( - key, fun, exc)) + '{0} in function {1}, error:\n{2}').format( + key, fun, trb)) continue if not isinstance(ret, dict): continue diff --git a/salt/log.py b/salt/log.py index 1b54787ab7..fb1a958e68 100644 --- a/salt/log.py +++ b/salt/log.py @@ -41,10 +41,12 @@ MODNAME_PATTERN = re.compile(r'(?P%%\(name\)(?:\-(?P[\d]+))?s)') __CONSOLE_CONFIGURED = False __LOGFILE_CONFIGURED = False + def is_console_configured(): global __CONSOLE_CONFIGURED return __CONSOLE_CONFIGURED + def is_logfile_configured(): global __LOGFILE_CONFIGURED return __LOGFILE_CONFIGURED @@ -118,10 +120,14 @@ def setup_console_logger(log_level='error', log_format=None, date_format=None): Setup the console logger ''' if is_console_configured(): - logging.getLogger(__name__).warning("Console logging already configured") + logging.getLogger(__name__).warn('Console logging already configured') return init() + + if log_level is None: + log_level = 'warning' + level = LOG_LEVELS.get(log_level.lower(), logging.ERROR) rootLogger = logging.getLogger() @@ -151,18 +157,21 @@ def setup_logfile_logger(log_path, log_level='error', log_format=None, ''' if is_logfile_configured(): - logging.getLogger(__name__).warning("Logfile logging already configured") + logging.getLogger(__name__).warn('Logfile logging already configured') return init() + + if log_level is None: + log_level = 'warning' + level = LOG_LEVELS.get(log_level.lower(), logging.ERROR) try: rootLogger = logging.getLogger() handler = getattr( - logging.handlers, 'WatchedFileHandler', logging.FileHandler)( - log_path, 'a', 'utf-8', delay=0 - ) + logging.handlers, 'WatchedFileHandler', logging.FileHandler + )(log_path, 'a', 'utf-8', delay=0) except (IOError, OSError): err = ('Failed to open log file, do you have permission to write to ' '{0}'.format(log_path)) @@ -175,7 +184,7 @@ def setup_logfile_logger(log_path, log_level='error', log_format=None, if not log_format: log_format = '%(asctime)s [%(name)-15s][%(levelname)-8s] %(message)s' if not date_format: - date_format = '%H:%M:%S' + date_format = '%Y-%m-%d %H:%M:%S' formatter = logging.Formatter(log_format, datefmt=date_format) diff --git a/salt/master.py b/salt/master.py index 1baccd4bc6..0e30be36dd 100644 --- a/salt/master.py +++ b/salt/master.py @@ -16,6 +16,9 @@ import logging import hashlib import tempfile import datetime +import pwd +import getpass +import resource import subprocess import multiprocessing @@ -37,6 +40,7 @@ import salt.pillar import salt.state import salt.runner import salt.utils.event +import salt.utils.verify from salt.utils.debug import enable_sigusr1_handler @@ -60,7 +64,7 @@ def clean_proc(proc, wait_for_kill=10): log.error(('Process did not die with terminate(): {0}' .format(proc.pid))) os.kill(signal.SIGKILL, proc.pid) - except (AssertionError, AttributeError) as e: + except (AssertionError, AttributeError): # Catch AssertionError when the proc is evaluated inside the child # Catch AttributeError when the process dies between proc.is_alive() # and proc.terminate() and turns into a NoneType @@ -98,19 +102,45 @@ class SMaster(object): A key needs to be placed in the filesystem with permissions 0400 so clients are required to run as root. ''' - log.info('Preparing the root key for local communication') - keyfile = os.path.join(self.opts['cachedir'], '.root_key') - if os.path.isfile(keyfile): - with open(keyfile, 'r') as fp_: - return fp_.read() - else: - key = salt.crypt.Crypticle.generate_key_string() + users = [] + keys = {} + acl_users = set(self.opts['client_acl'].keys()) + if self.opts.get('user'): + acl_users.add(self.opts['user']) + acl_users.add(getpass.getuser()) + for user in pwd.getpwall(): + users.append(user.pw_name) + for user in acl_users: + log.info( + 'Preparing the {0} key for local communication'.format( + user + ) + ) cumask = os.umask(191) + if not user in users: + log.error('ACL user {0} is not available'.format(user)) + continue + keyfile = os.path.join( + self.opts['cachedir'], '.{0}_key'.format(user) + ) + + if os.path.exists(keyfile): + log.debug('Removing stale keyfile: {0}'.format(keyfile)) + os.unlink(keyfile) + + key = salt.crypt.Crypticle.generate_key_string() with open(keyfile, 'w+') as fp_: fp_.write(key) os.umask(cumask) os.chmod(keyfile, 256) - return key + try: + os.chown(keyfile, pwd.getpwnam(user).pw_uid, -1) + except OSError: + # The master is not being run as root and can therefore not + # chown the key file + pass + keys[user] = key + return keys class Master(SMaster): @@ -152,13 +182,48 @@ class Master(SMaster): except KeyboardInterrupt: break + def __set_max_open_files(self): + # Let's check to see how our max open files(ulimit -n) setting is + mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE) + log.info( + 'Current values for max open files soft/hard setting: ' + '{0}/{1}'.format( + mof_s, mof_h + ) + ) + # Let's grab, from the configuration file, the value to raise max open + # files to + mof_c = self.opts['max_open_files'] + if mof_c > mof_h: + # The configured value is higher than what's allowed + log.warning( + 'The value for the \'max_open_files\' setting, {0}, is higher ' + 'than what the user running salt is allowed to raise to, {1}. ' + 'Defaulting to {1}.'.format(mof_c, mof_h) + ) + mof_c = mof_h + + if mof_s < mof_c: + # There's room to raise the value. Raise it! + log.warning('Raising max open files value to {0}'.format(mof_c)) + resource.setrlimit(resource.RLIMIT_NOFILE, (mof_c, mof_h)) + mof_s, mof_h = resource.getrlimit(resource.RLIMIT_NOFILE) + log.warning( + 'New values for max open files soft/hard values: ' + '{0}/{1}'.format(mof_s, mof_h) + ) + def start(self): ''' Turn on the master server components ''' + log.info( + 'salt-master is starting as user \'{0}\''.format(getpass.getuser()) + ) + enable_sigusr1_handler() - log.warn('Starting the Salt Master') + self.__set_max_open_files() clear_old_jobs_proc = multiprocessing.Process( target=self._clear_old_jobs) clear_old_jobs_proc.start() @@ -176,7 +241,6 @@ class Master(SMaster): SIGTERM is encountered. This is required when running a salt master under a process minder like daemontools ''' - mypid = os.getpid() log.warn(('Caught signal {0}, stopping the Salt Master' .format(signum))) clean_proc(clear_old_jobs_proc) @@ -227,7 +291,7 @@ class Publisher(multiprocessing.Process): except AttributeError: pub_sock.setsockopt(zmq.SNDHWM, 1) pub_sock.setsockopt(zmq.RCVHWM, 1) - pub_uri = 'tcp://{0[interface]}:{0[publish_port]}'.format(self.opts) + pub_uri = 'tcp://{interface}:{publish_port}'.format(**self.opts) # Prepare minion pull socket pull_sock = context.socket(zmq.PULL) pull_uri = 'ipc://{0}'.format( @@ -255,6 +319,24 @@ class Publisher(multiprocessing.Process): if exc.errno == errno.EINTR: continue raise exc + if self.opts['pub_refresh']: + pub_sock.close() + #time.sleep(0.5) + pub_sock = context.socket(zmq.PUB) + try: + pub_sock.setsockopt(zmq.HWM, 1) + except AttributeError: + pub_sock.setsockopt(zmq.SNDHWM, 1) + pub_sock.setsockopt(zmq.RCVHWM, 1) + con = False + while not con: + time.sleep(0.1) + try: + pub_sock.bind(pub_uri) + con = True + except zmq.ZMQError: + pass + except KeyboardInterrupt: pub_sock.close() pull_sock.close() @@ -270,7 +352,7 @@ class ReqServer(object): self.master_key = mkey self.context = zmq.Context(self.opts['worker_threads']) # Prepare the zeromq sockets - self.uri = 'tcp://%(interface)s:%(ret_port)s' % self.opts + self.uri = 'tcp://{interface}:{ret_port}'.format(**self.opts) self.clients = self.context.socket(zmq.ROUTER) self.workers = self.context.socket(zmq.DEALER) self.w_uri = 'ipc://{0}'.format( @@ -395,14 +477,14 @@ class MWorker(multiprocessing.Process): ''' Take care of a cleartext command ''' - log.info('Clear payload received with command %(cmd)s', load) + log.info('Clear payload received with command {cmd}'.format(**load)) return getattr(self.clear_funcs, load['cmd'])(load) def _handle_pub(self, load): ''' Handle a command sent via a public key pair ''' - log.info('Pubkey payload received with command %(cmd)s', load) + log.info('Pubkey payload received with command {cmd}'.format(**load)) def _handle_aes(self, load): ''' @@ -439,10 +521,7 @@ class AESFuncs(object): # def __init__(self, opts, crypticle): self.opts = opts - self.event = salt.utils.event.SaltEvent( - self.opts['sock_dir'], - 'master' - ) + self.event = salt.utils.event.MasterEvent(self.opts['sock_dir']) self.serial = salt.payload.Serial(opts) self.crypticle = crypticle # Make a client @@ -600,6 +679,18 @@ class AESFuncs(object): ret.append(os.path.relpath(root, path)) return ret + def _dir_list(self, load): + ''' + Return a list of all directories on the master + ''' + ret = [] + if load['env'] not in self.opts['file_roots']: + return ret + for path in self.opts['file_roots'][load['env']]: + for root, dirs, files in os.walk(path, followlinks=True): + ret.append(os.path.relpath(root, path)) + return ret + def _master_opts(self, load): ''' Return the master options to the minion @@ -652,7 +743,7 @@ class AESFuncs(object): if 'id' not in load or 'tag' not in load or 'data' not in load: return False tag = '{0}_{1}'.format(load['tag'], load['id']) - return self.event.fire_event(load['data'], tag) + return self.event.fire_event(load, tag) def _return(self, load): ''' @@ -661,7 +752,12 @@ class AESFuncs(object): # If the return data is invalid, just ignore it if 'return' not in load or 'jid' not in load or 'id' not in load: return False - log.info('Got return from {0[id]} for job {0[jid]}'.format(load)) + if load['jid'] == 'req': + # The minion is returning a standalone job, request a jobid + load['jid'] = salt.utils.prep_jid( + self.opts['cachedir'], + self.opts['hash_type']) + log.info('Got return from {id} for job {jid}'.format(**load)) self.event.fire_event(load, load['jid']) if not self.opts['job_cache']: return @@ -673,7 +769,7 @@ class AESFuncs(object): if not os.path.isdir(jid_dir): log.error( 'An inconsistency occurred, a job was received with a job id ' - 'that is not present on the master: %(jid)s', load + 'that is not present on the master: {jid}'.format(**load) ) return False hn_dir = os.path.join(jid_dir, load['id']) @@ -711,7 +807,7 @@ class AESFuncs(object): if not os.path.isdir(jid_dir): log.error( 'An inconsistency occurred, a job was received with a job id ' - 'that is not present on the master: %(jid)s', load + 'that is not present on the master: {jid}'.format(**load) ) return False wtag = os.path.join(jid_dir, 'wtag_{0}'.format(load['id'])) @@ -727,6 +823,7 @@ class AESFuncs(object): return False # Format individual return loads + self.event.fire_event({'syndic': load['return'].keys()}, load['jid']) for key, item in load['return'].items(): ret = {'jid': load['jid'], 'id': key, @@ -893,8 +990,8 @@ class AESFuncs(object): os.path.join(self.opts['sock_dir'], 'publish_pull.ipc') ) pub_sock.connect(pull_uri) - log.info(('Publishing minion job: #{0[jid]}, func: "{0[fun]}", args:' - ' "{0[arg]}", target: "{0[tgt]}"').format(load)) + log.info(('Publishing minion job: #{jid}, func: "{fun}", args:' + ' "{arg}", target: "{tgt}"').format(**load)) pub_sock.send(self.serial.dumps(payload)) # Run the client get_returns method based on the form data sent if 'form' in clear_load: @@ -960,10 +1057,7 @@ class ClearFuncs(object): self.master_key = master_key self.crypticle = crypticle # Create the event manager - self.event = salt.utils.event.SaltEvent( - self.opts['sock_dir'], - 'master' - ) + self.event = salt.utils.event.MasterEvent(self.opts['sock_dir']) # Make a client self.local = salt.client.LocalClient(self.opts['conf_file']) @@ -978,7 +1072,7 @@ class ClearFuncs(object): 0, 'list' ) - log.debug('Cluster distributed: %s', ret) + log.debug('Cluster distributed: {0}'.format(ret)) def _cluster_load(self): ''' @@ -1025,10 +1119,11 @@ class ClearFuncs(object): fmode = os.stat(filename) if os.getuid() == 0: - if not fmode.st_uid == uid or not fmode.st_gid == gid: - if self.opts.get('permissive_pki_access', False) \ - and fmode.st_gid in groups: - return True + if fmode.st_uid == uid or not fmode.st_gid == gid: + return True + elif self.opts.get('permissive_pki_access', False) \ + and fmode.st_gid in groups: + return True else: if stat.S_IWOTH & fmode.st_mode: # don't allow others to write to the file @@ -1092,16 +1187,20 @@ class ClearFuncs(object): Authenticate the client, use the sent public key to encrypt the aes key which was generated at start up. - This method fires an event over the master event manager. The evnt is + This method fires an event over the master event manager. The event is tagged "auth" and returns a dict with information about the auth event ''' + # 0. Check for max open files # 1. Verify that the key we are receiving matches the stored key # 2. Store the key if it is not there # 3. make an rsa key with the pub key # 4. encrypt the aes key as an encrypted salt.payload # 5. package the return and return it - log.info('Authentication request from %(id)s', load) + + salt.utils.verify.check_max_open_files(self.opts) + + log.info('Authentication request from {id}'.format(**load)) pubfn = os.path.join(self.opts['pki_dir'], 'minions', load['id']) @@ -1119,9 +1218,9 @@ class ClearFuncs(object): # The key has been accepted check it if not open(pubfn, 'r').read() == load['pub']: log.error( - 'Authentication attempt from %(id)s failed, the public ' + 'Authentication attempt from {id} failed, the public ' 'keys did not match. This may be an attempt to compromise ' - 'the Salt cluster.', load + 'the Salt cluster.'.format(**load) ) ret = {'enc': 'clear', 'load': {'ret': False}} @@ -1132,7 +1231,7 @@ class ClearFuncs(object): return ret elif os.path.isfile(pubfn_rejected): # The key has been rejected, don't place it in pending - log.info('Public key rejected for %(id)s', load) + log.info('Public key rejected for {id}'.format(**load)) ret = {'enc': 'clear', 'load': {'ret': False}} eload = {'result': False, @@ -1143,7 +1242,7 @@ class ClearFuncs(object): elif not os.path.isfile(pubfn_pend)\ and not self._check_autosign(load['id']): # This is a new key, stick it in pre - log.info('New public key placed in pending for %(id)s', load) + log.info('New public key placed in pending for {id}'.format(**load)) with open(pubfn_pend, 'w+') as fp_: fp_.write(load['pub']) ret = {'enc': 'clear', @@ -1160,9 +1259,9 @@ class ClearFuncs(object): # ret False if not open(pubfn_pend, 'r').read() == load['pub']: log.error( - 'Authentication attempt from %(id)s failed, the public ' + 'Authentication attempt from {id} failed, the public ' 'keys in pending did not match. This may be an attempt to ' - 'compromise the Salt cluster.', load + 'compromise the Salt cluster.'.format(**load) ) eload = {'result': False, 'id': load['id'], @@ -1172,9 +1271,8 @@ class ClearFuncs(object): 'load': {'ret': False}} else: log.info( - 'Authentication failed from host %(id)s, the key is in ' - 'pending and needs to be accepted with salt-key -a %(id)s', - load + 'Authentication failed from host {id}, the key is in ' + 'pending and needs to be accepted with salt-key -a {id}'.format(**load) ) eload = {'result': True, 'act': 'pend', @@ -1188,9 +1286,9 @@ class ClearFuncs(object): # This key is in pending, if it is the same key auto accept it if not open(pubfn_pend, 'r').read() == load['pub']: log.error( - 'Authentication attempt from %(id)s failed, the public ' + 'Authentication attempt from {id} failed, the public ' 'keys in pending did not match. This may be an attempt to ' - 'compromise the Salt cluster.', load + 'compromise the Salt cluster.'.format(**load) ) eload = {'result': False, 'id': load['id'], @@ -1214,7 +1312,7 @@ class ClearFuncs(object): return {'enc': 'clear', 'load': {'ret': False}} - log.info('Authentication accepted from %(id)s', load) + log.info('Authentication accepted from {id}'.format(**load)) with open(pubfn, 'w+') as fp_: fp_.write(load['pub']) pub = None @@ -1247,8 +1345,40 @@ class ClearFuncs(object): by the LocalClient. ''' # Verify that the caller has root on master - if not clear_load.pop('key') == self.key: - return '' + if 'user' in clear_load: + if clear_load['user'].startswith('sudo_'): + if not clear_load.pop('key') == self.key.get(getpass.getuser(), ''): + return '' + elif clear_load['user'] == self.opts.get('user', 'root'): + if not clear_load.pop('key') == self.key[self.opts.get('user', 'root')]: + return '' + elif clear_load['user'] == getpass.getuser(): + if not clear_load.pop('key') == self.key.get(getpass.getuser()): + return '' + else: + if clear_load['user'] in self.key: + # User is authorised, check key and check perms + if not clear_load.pop('key') == self.key[clear_load['user']]: + return '' + good = False + for user in self.opts['client_acl']: + if clear_load['user'] != user: + continue + for regex in self.opts['client_acl'][user]: + if re.match(regex, clear_load['fun']): + good = True + if not good: + return '' + else: + return '' + else: + if not clear_load.pop('key') == self.key[getpass.getuser()]: + return '' + if not clear_load['jid']: + clear_load['jid'] = salt.utils.prep_jid( + self.opts['cachedir'], + self.opts['hash_type'] + ) jid_dir = salt.utils.jid_dir( clear_load['jid'], self.opts['cachedir'], @@ -1286,12 +1416,12 @@ class ClearFuncs(object): load['to'] = clear_load['to'] if 'user' in clear_load: - log.info(('User {0[user]} Published command {0[fun]} with jid' - ' {0[jid]}').format(clear_load)) + log.info(('User {user} Published command {fun} with jid' + ' {jid}').format(**clear_load)) load['user'] = clear_load['user'] else: - log.info(('Published command {0[fun]} with jid' - ' {0[jid]}').format(clear_load)) + log.info(('Published command {fun} with jid' + ' {jid}').format(**clear_load)) log.debug('Published command details {0}'.format(load)) payload['load'] = self.crypticle.dumps(load) diff --git a/salt/minion.py b/salt/minion.py index 6c924c3b6d..af110da3d2 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -5,14 +5,17 @@ Routines to set up a minion # Import python libs import logging +import getpass import multiprocessing import fnmatch import os +import hashlib import re import threading import time import traceback +import sys # Import third party libs import zmq @@ -109,7 +112,7 @@ class SMinion(object): self.opts['environment'], ).compile_pillar() self.functions = salt.loader.minion_mods(self.opts) - self.returners = salt.loader.returners(self.opts) + self.returners = salt.loader.returners(self.opts, self.functions) self.states = salt.loader.states(self.opts, self.functions) self.rend = salt.loader.render(self.opts, self.functions) self.matcher = Matcher(self.opts, self.functions) @@ -131,10 +134,6 @@ class Minion(object): self.functions, self.returners = self.__load_modules() self.matcher = Matcher(self.opts, self.functions) self.proc_dir = get_proc_dir(opts['cachedir']) - if hasattr(self, '_syndic') and self._syndic: - log.warn('Starting the Salt Syndic Minion') - else: - log.warn('Starting the Salt Minion') self.authenticate() opts['pillar'] = salt.pillar.get_pillar( opts, @@ -161,7 +160,7 @@ class Minion(object): ''' self.opts['grains'] = salt.loader.grains(self.opts) functions = salt.loader.minion_mods(self.opts) - returners = salt.loader.returners(self.opts) + returners = salt.loader.returners(self.opts, functions) return functions, returners def _handle_payload(self, payload): @@ -230,36 +229,39 @@ class Minion(object): if isinstance(data['fun'], string_types): if data['fun'] == 'sys.reload_modules': self.functions, self.returners = self.__load_modules() - - if self.opts['multiprocessing']: - if isinstance(data['fun'], tuple) or isinstance(data['fun'], list): - multiprocessing.Process( - target=lambda: self._thread_multi_return(data) - ).start() - else: - multiprocessing.Process( - target=lambda: self._thread_return(data) - ).start() + if isinstance(data['fun'], tuple) or isinstance(data['fun'], list): + target = Minion._thread_multi_return else: - if isinstance(data['fun'], tuple) or isinstance(data['fun'], list): - threading.Thread( - target=lambda: self._thread_multi_return(data) - ).start() - else: - threading.Thread( - target=lambda: self._thread_return(data) - ).start() + target = Minion._thread_return + # We stash an instance references to allow for the socket + # communication in Windows. You can't pickle functions, and thus + # python needs to be able to reconstruct the reference on the other + # side. + instance = self + if self.opts['multiprocessing']: + if sys.platform.startswith('win'): + # let python reconstruct the minion on the other side if we're + # running on windows + instance = None + multiprocessing.Process(target=target, args=(instance, self.opts, data)).start() + else: + threading.Thread(target=target, args=(instance, self.opts, data)).start() - def _thread_return(self, data): + @classmethod + def _thread_return(class_, minion_instance, opts, data): ''' This method should be used as a threading target, start the actual minion side execution. ''' - if self.opts['multiprocessing']: - fn_ = os.path.join(self.proc_dir, data['jid']) + # this seems awkward at first, but it's a workaround for Windows + # multiprocessing communication. + if not minion_instance: + minion_instance = class_(opts) + if opts['multiprocessing']: + fn_ = os.path.join(minion_instance.proc_dir, data['jid']) sdata = {'pid': os.getpid()} sdata.update(data) - open(fn_, 'w+').write(self.serial.dumps(sdata)) + open(fn_, 'w+').write(minion_instance.serial.dumps(sdata)) ret = {} for ind in range(0, len(data['arg'])): try: @@ -274,11 +276,13 @@ class Minion(object): pass function_name = data['fun'] - if function_name in self.functions: + if function_name in minion_instance.functions: + ret['success'] = False try: - func = self.functions[data['fun']] + func = minion_instance.functions[data['fun']] args, kw = detect_kwargs(func, data['arg'], data) ret['return'] = func(*args, **kw) + ret['success'] = True except CommandNotFoundError as exc: msg = 'Command required for \'{0}\' not found: {1}' log.debug(msg.format(function_name, str(exc))) @@ -301,12 +305,12 @@ class Minion(object): ret['jid'] = data['jid'] ret['fun'] = data['fun'] - self._return_pub(ret) + minion_instance._return_pub(ret) if data['ret']: for returner in set(data['ret'].split(',')): - ret['id'] = self.opts['id'] + ret['id'] = opts['id'] try: - self.returners[returner](ret) + minion_instance.returners[returner](ret) except Exception as exc: log.error( 'The return failed for job {0} {1}'.format( @@ -315,12 +319,20 @@ class Minion(object): ) ) - def _thread_multi_return(self, data): + @classmethod + def _thread_multi_return(class_, minion_instance, opts, data): ''' This method should be used as a threading target, start the actual minion side execution. ''' - ret = {'return': {}} + # this seems awkward at first, but it's a workaround for Windows + # multiprocessing communication. + if not minion_instance: + minion_instance = class_(opts) + ret = { + 'return': {}, + 'success': {}, + } for ind in range(0, len(data['fun'])): for index in range(0, len(data['arg'][ind])): try: @@ -334,10 +346,12 @@ class Minion(object): except Exception: pass + ret['success'][data['fun'][ind]] = False try: - func = self.functions[data['fun'][ind]] + func = minion_instance.functions[data['fun'][ind]] args, kw = detect_kwargs(func, data['arg'][ind], data) ret['return'][data['fun'][ind]] = func(*args, **kw) + ret['success'][data['fun'][ind]] = True except Exception as exc: trb = traceback.format_exc() log.warning( @@ -347,12 +361,12 @@ class Minion(object): ) ret['return'][data['fun'][ind]] = trb ret['jid'] = data['jid'] - self._return_pub(ret) + minion_instance._return_pub(ret) if data['ret']: for returner in set(data['ret'].split(',')): - ret['id'] = self.opts['id'] + ret['id'] = opts['id'] try: - self.returners[returner](ret) + minion_instance.returners[returner](ret) except Exception as exc: log.error( 'The return failed for job {0} {1}'.format( @@ -417,6 +431,23 @@ class Minion(object): open(fn_, 'w+').write(self.serial.dumps(ret)) return ret_val + def _state_run(self): + ''' + Execute a state run based on information set in the minion config file + ''' + if self.opts['startup_states']: + data = {'jid': 'req', 'ret': ''} + if self.opts['startup_states'] == 'sls': + data['fun'] = 'state.sls' + data['arg'] = [self.opts['sls_list']] + elif self.opts['startup_states'] == 'top': + data['fun'] = 'state.top' + data['arg'] = [self.opts['top_file']] + else: + data['fun'] = 'state.highstate' + data['arg'] = [] + self._handle_decoded_payload(data) + @property def master_pub(self): return 'tcp://{ip}:{port}'.format(ip=self.opts['master_ip'], @@ -429,7 +460,9 @@ class Minion(object): in, signing in can occur as often as needed to keep up with the revolving master aes key. ''' - log.debug('Attempting to authenticate with the Salt Master') + log.debug('Attempting to authenticate with the Salt Master at {0}'.format( + self.opts['master_ip'] + )) auth = salt.crypt.Auth(self.opts) while True: creds = auth.sign_in() @@ -468,34 +501,65 @@ class Minion(object): ''' Lock onto the publisher. This is the main event loop for the minion ''' + log.info( + '{0} is starting as user \'{1}\''.format( + self.__class__.__name__, + getpass.getuser() + ) + ) + log.debug('Minion "{0}" trying to tune in'.format(self.opts['id'])) context = zmq.Context() # Prepare the minion event system # # Start with the publish socket - epub_sock = context.socket(zmq.PUB) - epub_uri = 'ipc://{0}'.format( - os.path.join(self.opts['sock_dir'], 'minion_event_pub.ipc') + id_hash = hashlib.md5(self.opts['id']).hexdigest() + epub_sock_path = os.path.join( + self.opts['sock_dir'], + 'minion_event_{0}_pub.ipc'.format(id_hash) ) + epull_sock_path = os.path.join( + self.opts['sock_dir'], + 'minion_event_{0}_pull.ipc'.format(id_hash) + ) + epub_sock = context.socket(zmq.PUB) + if self.opts.get('ipc_mode', '') == 'tcp': + epub_uri = 'tcp://127.0.0.1:{0}'.format( + self.opts['tcp_pub_port'] + ) + epull_uri = 'tcp://127.0.0.1:{0}'.format( + self.opts['tcp_pull_port'] + ) + else: + epub_uri = 'ipc://{0}'.format(epub_sock_path) + epull_uri = 'ipc://{0}'.format(epull_sock_path) + + log.debug( + '{0} PUB socket URI: {1}'.format( + self.__class__.__name__, epub_uri + ) + ) + log.debug( + '{0} PULL socket URI: {1}'.format( + self.__class__.__name__, epull_uri + ) + ) + # Create the pull socket epull_sock = context.socket(zmq.PULL) - epull_uri = 'ipc://{0}'.format( - os.path.join(self.opts['sock_dir'], 'minion_event_pull.ipc') - ) # Bind the event sockets epub_sock.bind(epub_uri) epull_sock.bind(epull_uri) # Restrict access to the sockets - os.chmod( - os.path.join(self.opts['sock_dir'], - 'minion_event_pub.ipc'), - 448 - ) - os.chmod( - os.path.join(self.opts['sock_dir'], - 'minion_event_pull.ipc'), - 448 - ) + if not self.opts.get('ipc_mode', '') == 'tcp': + os.chmod( + epub_sock_path, + 448 + ) + os.chmod( + epull_sock_path, + 448 + ) poller = zmq.Poller() epoller = zmq.Poller() @@ -510,12 +574,16 @@ class Minion(object): # Make sure to gracefully handle SIGUSR1 enable_sigusr1_handler() + # On first startup execute a state run if configured to do so + self._state_run() + if self.opts['sub_timeout']: last = time.time() while True: try: - socks = dict(poller.poll(self.opts['sub_timeout'])) + socks = dict(poller.poll(self.opts['sub_timeout'] * 1000)) if socket in socks and socks[socket] == zmq.POLLIN: + self.passive_refresh() payload = self.serial.loads(socket.recv()) self._handle_payload(payload) last = time.time() @@ -540,7 +608,6 @@ class Minion(object): last = time.time() time.sleep(0.05) multiprocessing.active_children() - self.passive_refresh() # Check the event system if epoller.poll(1): try: @@ -548,12 +615,12 @@ class Minion(object): epub_sock.send(package) except Exception: pass - except Exception as exc: + except Exception: log.critical(traceback.format_exc()) else: while True: try: - socks = dict(poller.poll(60)) + socks = dict(poller.poll(60000)) if socket in socks and socks[socket] == zmq.POLLIN: payload = self.serial.loads(socket.recv()) self._handle_payload(payload) @@ -568,9 +635,8 @@ class Minion(object): epub_sock.send(package) except Exception: pass - except Exception as exc: + except Exception: log.critical(traceback.format_exc()) - class Syndic(salt.client.LocalClient, Minion): diff --git a/salt/modules/aliases.py b/salt/modules/aliases.py index 1b4e8b3386..b5d2747c12 100644 --- a/salt/modules/aliases.py +++ b/salt/modules/aliases.py @@ -68,9 +68,11 @@ def __write_aliases_file(lines): if not line_comment: line_comment = '' if line_alias and line_target: - out.write('%s: %s%s\n' % (line_alias, line_target, line_comment)) + out.write('{0}: {1}{2}\n'.format( + line_alias, line_target, line_comment + )) else: - out.write('%s\n' % line_comment) + out.write('{0}\n'.format(line_comment)) out.close() os.rename(out.name, afn) diff --git a/salt/modules/apache.py b/salt/modules/apache.py index fd912ff274..681b1a49f0 100644 --- a/salt/modules/apache.py +++ b/salt/modules/apache.py @@ -24,7 +24,7 @@ def _detect_os(): ''' Apache commands and paths differ depending on packaging ''' - httpd = ('CentOS', 'Scientific', 'RedHat', 'Fedora', 'Arch') + httpd = ('CentOS', 'Scientific', 'RedHat', 'Fedora', 'Arch', 'CloudLinux') apache2 = ('Ubuntu', 'Debian',) if __grains__['os'] in httpd: return 'apachectl' diff --git a/salt/modules/apt.py b/salt/modules/apt.py index cc98aa703d..5b37432213 100644 --- a/salt/modules/apt.py +++ b/salt/modules/apt.py @@ -17,7 +17,7 @@ def __virtual__(): return 'pkg' if __grains__['os'] in ('Debian', 'Ubuntu') else False -def __init__(): +def __init__(opts): ''' For Debian and derivative systems, set up a few env variables to keep apt happy and @@ -43,15 +43,11 @@ def available_version(name): salt '*' pkg.available_version ''' version = '' - cmd = 'apt-cache -q show {0} | grep ^Version'.format(name) + cmd = 'apt-cache -q policy {0} | grep Candidate'.format(name) out = __salt__['cmd.run_stdout'](cmd) version_list = out.split() - for comp in version_list: - if comp == 'Version:': - continue - return comp if len(version_list) >= 2: version = version_list[-1] diff --git a/salt/modules/at.py b/salt/modules/at.py new file mode 100644 index 0000000000..bc1d269311 --- /dev/null +++ b/salt/modules/at.py @@ -0,0 +1,255 @@ +''' +Wrapper module for at(1) + +Also, a 'tag' feature has been added to more +easily tag jobs. +''' + +import re +import datetime +import time + +__outputter__ = { + 'atc': 'txt', +} + +# OS Families that should work (Ubuntu and Debian are the +# default) + +# Tested on CentOS 5.8 +rhel = ('CentOS', 'Scientific', 'RedHat', 'Fedora', 'CloudLinux') + +# Tested on OpenBSD 5.0 +bsd = ('OpenBSD', 'FreeBSD') + +# Known not to work +bad = ('Windows') + + +def __virtual__(): + ''' + Most everything has the ability to support at(1) + ''' + return False if __grains__['os'] in bad else 'at' + + +def _cmd(bin, *opts): + ''' + Wrapper to run at(1) or return None. + ''' + bin = __salt__['cmd.which'](bin) + if bin: + return __salt__['cmd.run_stdout']('{0} {1}'.format(bin, + ' '.join(opts))) + + +def atq(tag=None): + ''' + List all queued and running jobs or only those with + an optional 'tag'. + + CLI Example:: + + salt '*' at.atq + salt '*' at.atq [tag] + salt '*' at.atq [job number] + ''' + jobs = [] + + # Shim to produce output similar to what __virtual__() should do + # but __salt__ isn't available in __virtual__() + if __grains__['os'] in rhel: + output = _cmd('at', '-l') + else: + output = _cmd('atq') + + if output == None: + return '"{0}" is not available.'.format('at.atq') + + # No jobs so return + if output == '': + return {'jobs': jobs} + + # Split each job into a dictionary and handle + # pulling out tags or only listing jobs with a certain + # tag + for line in output.split('\n'): + job_tag = '' + + # Jobs created with at.at() will use the following + # comment to denote a tagged job. + job_kw_regex = re.compile('^### SALT: (\w+)') + + # Redhat/CentOS + if __grains__['os'] in rhel: + job, spec = line.split('\t') + specs = spec.split() + elif __grains__['os'] in bsd: + if line.startswith(' Rank'): + continue + else: + tmp = line.split() + timestr = ' '.join(tmp[1:5]) + job = tmp[6] + specs = datetime.datetime(*(time.strptime(timestr, '%b %d, %Y ' + '%H:%M')[0:5])).isoformat().split('T') + specs.append(tmp[7]) + specs.append(tmp[5]) + + else: + job, spec = line.split('\t') + tmp = spec.split() + timestr = ' '.join(tmp[0:5]) + specs = datetime.datetime(*(time.strptime(timestr) + [0:5])).isoformat().split('T') + specs.append(tmp[5]) + specs.append(tmp[6]) + + # Search for any tags + atc_out = _cmd('at', '-c', job) + for line in atc_out.split('\n'): + tmp = job_kw_regex.match(line) + if tmp: + job_tag = tmp.groups()[0] + + if __grains__['os'] in bsd: + job = str(job) + else: + job = int(job) + + # If a tag is supplied, only list jobs with that tag + if tag: + + # TODO: Looks like there is a difference between salt and salt-call + # If I don't wrap job in an int(), it fails on salt but works on + # salt-call. With the int(), it fails with salt-call but not salt. + if tag == job_tag or tag == job: + jobs.append({'job': job, 'date': specs[0], 'time': specs[1], + 'queue': specs[2], 'user': specs[3], 'tag': job_tag}) + else: + jobs.append({'job': job, 'date': specs[0], 'time': specs[1], + 'queue': specs[2], 'user': specs[3], 'tag': job_tag}) + + return {'jobs': jobs} + + +def atrm(*pargs): + ''' + Remove jobs from the queue. + + CLI Example:: + + salt '*' at.atrm .. + salt '*' at.atrm all + salt '*' at.atrm all [tag] + ''' + opts = '' + + # Need to do this here also since we use atq() + if not __salt__['cmd.which']('at'): + return '"{0}" is not available.'.format('at.atrm') + + if not pargs: + return {'jobs': {'removed': [], 'tag': None}} + + if pargs[0] == 'all': + if len(pargs) > 1: + opts = map(str, [j['job'] for j in atq(pargs[1])['jobs']]) + ret = {'jobs': {'removed': opts, 'tag': pargs[1]}} + else: + opts = map(str, [j['job'] for j in atq()['jobs']]) + ret = {'jobs': {'removed': opts, 'tag': None}} + else: + opts = map(str, [i['job'] for i in atq()['jobs'] + if i['job'] in pargs]) + ret = {'jobs': {'removed': opts, 'tag': None}} + + # Shim to produce output similar to what __virtual__() should do + # but __salt__ isn't available in __virtual__() + output = _cmd('at', '-d', ' '.join(opts)) + if output == None: + return '"{0}" is not available.'.format('at.atrm') + + return ret + + +def at(*pargs, **kwargs): + ''' + Add a job to the queue. + + The 'timespec' follows the format documented in the + at(1) manpage. + + CLI Example:: + + salt '*' at.at [tag=] [runas=] + salt '*' at.at 12:05am '/sbin/reboot' tag=reboot + salt '*' at.at '3:05am +3 days' 'bin/myscript' tag=nightly runas=jim + ''' + echo_cmd = '' + + if len(pargs) < 2: + return {'jobs': []} + + # Shim to produce output similar to what __virtual__() should do + # but __salt__ isn't available in __virtual__() + bin = __salt__['cmd.which']('at') + if not bin: + return '"{0}" is not available.'.format('at.at') + + if __grains__['os'] in rhel: + echo_cmd = 'echo -e' + else: + echo_cmd = 'echo' + + if 'tag' in kwargs: + cmd = '{4} "### SALT: {0}\n{1}" | {2} {3}'.format(kwargs['tag'], + ' '.join(pargs[1:]), bin, pargs[0], echo_cmd) + else: + cmd = '{3} "{1}" | {2} {0}'.format(pargs[0], ' '.join(pargs[1:]), + bin, echo_cmd) + + # Can't use _cmd here since we need to prepend 'echo_cmd' + if 'runas' in kwargs: + output = __salt__['cmd.run']('{0}'.format(cmd), runas=kwargs['runas']) + else: + output = __salt__['cmd.run']('{0}'.format(cmd)) + + if output == None: + return '"{0}" is not available.'.format('at.at') + + if output.endswith('Garbled time'): + return {'jobs': [], 'error': 'invalid timespec'} + + if output.startswith('warning: commands'): + output = output.split('\n')[1] + + if output.startswith('commands will be executed'): + output = output.split('\n')[1] + + if __grains__['os'] in bsd: + return atq(str(output.split()[1])) + else: + return atq(int(output.split()[1])) + + +def atc(jobid): + ''' + Print the at(1) script that will run for the passed job + id. This is mostly for debugging so the output will + just be text. + + CLI Example:: + + salt '*' at.atc + ''' + # Shim to produce output similar to what __virtual__() should do + # but __salt__ isn't available in __virtual__() + output = _cmd('at', '-c', str(jobid)) + if output == None: + return '"{0}" is not available.'.format('at.atc') + + if output == '': + return {'error': 'invalid job id "{0}"'.format(str(jobid))} + + return output diff --git a/salt/modules/augeas_cfg.py b/salt/modules/augeas_cfg.py index 10c4993816..3f410cdd86 100644 --- a/salt/modules/augeas_cfg.py +++ b/salt/modules/augeas_cfg.py @@ -1,22 +1,28 @@ ''' Manages configuration files via augeas ''' +# Load Augeas libs +load = False +try: + from augeas import Augeas + load = True +except ImportError: + pass def __virtual__(): - ''' Only run this module if the augeas python module is installed ''' - try: - from augeas import Augeas - _ = Augeas - except ImportError: - return False + ''' + Only run this module if the augeas python module is installed + ''' + if load: + return 'augeas' else: - return "augeas" + return False def _recurmatch(path, aug): ''' - recursive generator providing the infrastructure for + Recursive generator providing the infrastructure for augtools print behaviour. This function is based on test_augeas.py from @@ -27,7 +33,7 @@ def _recurmatch(path, aug): clean_path = path.rstrip('/*') yield (clean_path, aug.get(path)) - for i in aug.match(clean_path + "/*"): + for i in aug.match(clean_path + '/*'): i = i.replace('!', '\!') # escape some dirs for x in _recurmatch(i, aug): yield x @@ -60,7 +66,7 @@ def get(path, value=''): path = path.rstrip('/') if value: - path += "/{0}".format(value.strip('/')) + path += '/{0}'.format(value.strip('/')) try: _match = aug.match(path) @@ -106,8 +112,6 @@ def setvalue(*args): %wheel ALL = PASSWD : ALL , NOPASSWD : /usr/bin/apt-get , /usr/bin/aptitude ''' - - from augeas import Augeas aug = Augeas() @@ -151,7 +155,6 @@ def match(path, value=''): salt '*' augeas.match /files/etc/services/service-name ssh ''' - from augeas import Augeas aug = Augeas() @@ -205,7 +208,6 @@ def ls(path): salt '*' augeas.ls /files/etc/passwd ''' - def _match(path): ''' Internal match function ''' try: @@ -237,7 +239,6 @@ def ls(path): def tree(path): - ''' Returns recursively the complete tree of a node @@ -245,7 +246,6 @@ def tree(path): salt '*' augeas.tree /files/etc/ ''' - from augeas import Augeas aug = Augeas() diff --git a/salt/modules/bluez.py b/salt/modules/bluez.py index 8a972cb4da..8bcb455250 100644 --- a/salt/modules/bluez.py +++ b/salt/modules/bluez.py @@ -36,10 +36,12 @@ def address(): salt '*' bluetooth.address ''' - cmd = "dbus-send --system --print-reply --dest=org.bluez / org.bluez.Manager.DefaultAdapter|awk '/object path/ {print $3}' | sed 's/\"//g'" + cmd = ('dbus-send --system --print-reply --dest=org.bluez / ' + 'org.bluez.Manager.DefaultAdapter|awk \'/object path/ ' + '{print $3}\' | sed \'s/"//g\'') path = __salt__['cmd.run'](cmd).split('\n') devname = path[0].split('/') - syspath = '/sys/class/bluetooth/%s/address' % devname[-1] + syspath = '/sys/class/bluetooth/{0}/address'.format(devname[-1]) sysfile = open(syspath, 'r') address = sysfile.read().strip() sysfile.close() @@ -84,7 +86,9 @@ def pair(address, key): to pair with, and 1234 is the passphrase. ''' address = address() - cmd = 'echo "%s" | bluez-simple-agent %s %s' % (address['devname'], address, key) + cmd = 'echo "{0}" | bluez-simple-agent {1} {2}'.format( + address['devname'], address, key + ) out = __salt__['cmd.run'](cmd).split('\n') return out @@ -101,7 +105,7 @@ def unpair(address): to unpair. ''' address = address() - cmd = 'bluez-test-device remove %s' % address + cmd = 'bluez-test-device remove {0}'.format(address) out = __salt__['cmd.run'](cmd).split('\n') return out diff --git a/salt/modules/ca.py b/salt/modules/ca.py new file mode 100644 index 0000000000..0bfb6e8b3d --- /dev/null +++ b/salt/modules/ca.py @@ -0,0 +1,568 @@ +''' +A salt interface for running a Certificate Authority (CA) +which provides signed/unsigned SSL certificates + +REQUIREMENT 1: + +Required python modules: PyOpenSSL + +REQUIREMENT 2: + +Add the following values in /etc/salt/minion for the +CA module to function properly:: + +ca.cert_base_path: '/etc/pki/koji' +''' + +# Import Python libs +import os +import time +import logging +import hashlib + +has_ssl = False +try: + import OpenSSL + has_ssl = True +except ImportError: + pass + +# Import Salt libs +from salt.exceptions import CommandExecutionError + +log = logging.getLogger(__name__) + +def __virtual__(): + ''' + Only load this module if the ca config options are set + ''' + if has_ssl: + return 'ca' + return False + + +def _cert_base_path(): + ''' + Return the base path for certs + ''' + if 'ca.cert_base_path' in __opts__: + return __opts__['ca.cert_base_path'] + raise CommandExecutionError( + "Please set the 'ca.cert_base_path' in the minion configuration" + ) + + +def _new_serial(ca_name, CN): + ''' + Return a serial number in hex using md5sum, based upon the the ca_name and + CN values + + ca_name + name of the CA + CN + common name in the request + ''' + hashnum = int( + hashlib.md5( + '{0}_{1}_{2}'.format( + ca_name, + CN, + int(time.time())) + ).hexdigest(), + 16 + ) + log.debug('Hashnum: {0}'.format(hashnum)) + + # record the hash somewhere + cachedir = __opts__['cachedir'] + log.debug('cachedir: {0}'.format(cachedir)) + serial_file = '{0}/{1}.serial'.format(cachedir, ca_name) + with open(serial_file, 'a+') as f: + f.write(str(hashnum)) + + return hashnum + +def _write_cert_to_database(ca_name, cert): + ''' + write out the index.txt database file in the appropriate directory to + track certificates + + ca_name + name of the CA + cert + certificate to be recorded + ''' + index_file = "{0}/{1}/index.txt".format(_cert_base_path(), ca_name) + + expire_date = cert.get_notAfter() + serial_number = cert.get_serial_number() + + #gotta prepend a / + subject = '/' + + # then we can add the rest of the subject + subject += '/'.join( + ['{0}={1}'.format( + x,y + ) for x,y in cert.get_subject().get_components()] + ) + subject += '\n' + + index_data = 'V\t{0}\t\t{1}\tunknown\t{2}'.format( + expire_date, + serial_number, + subject + ) + + with open(index_file, 'a+') as f: + f.write(index_data) + + +def _ca_exists(ca_name): + ''' + Verify whether a Certificate Authority (CA) already exists + + ca_name + name of the CA + ''' + + if os.path.exists('{0}/{1}/{2}_ca_cert.crt'.format( + _cert_base_path(), + ca_name, + ca_name + )): + return True + return False + + +def create_ca( + ca_name, + bits=2048, + days=365, + CN='localhost', + C='US', + ST='Utah', + L='Salt Lake City', + O='Salt Stack', + OU=None, + emailAddress='xyz@pdq.net'): + ''' + Create a Certificate Authority (CA) + + ca_name + name of the CA + bits + number of RSA key bits, default is 2048 + days + number of days the CA will be valid, default is 365 + CN + common name in the request, default is "localhost" + C + country, default is "US" + ST + state, default is "Utah" + L + locality, default is "Centerville", the city where SaltStack originated + O + organization, default is "Salt Stack" + OU + organizational unit, default is None + email + email address for the CA owner, default is 'xyz@pdq.net' + + Writes out a CA certificate based upon defined config values. If the file + already exists, the function just returns assuming the CA certificate + already exists. + + If the following values were set: + + ca.cert_base_path='/etc/pki/koji' + ca_name='koji' + + the resulting CA would be written in the following location:: + + /etc/pki/koji/koji_ca_cert.crt + ''' + if _ca_exists(ca_name): + return 'Certificate for CA named "{0}" already exists'.format(ca_name) + + if not os.path.exists('{0}/{1}'.format(_cert_base_path(), ca_name)): + os.makedirs('{0}/{1}'.format(_cert_base_path(), ca_name)) + + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) + + ca = OpenSSL.crypto.X509() + ca.set_version(3) + ca.set_serial_number(_new_serial(ca_name, CN)) + ca.get_subject().C = C + ca.get_subject().ST = ST + ca.get_subject().L = L + ca.get_subject().O = O + if OU: + ca.get_subject().OU = OU + ca.get_subject().CN = CN + ca.get_subject().emailAddress = emailAddress + + ca.gmtime_adj_notBefore(0) + ca.gmtime_adj_notAfter(days * 24 * 60 * 60) + ca.set_issuer(ca.get_subject()) + ca.set_pubkey(key) + + ca.add_extensions([ + OpenSSL.crypto.X509Extension('basicConstraints', True, + 'CA:TRUE, pathlen:0'), + OpenSSL.crypto.X509Extension('keyUsage', True, + 'keyCertSign, cRLSign'), + OpenSSL.crypto.X509Extension('subjectKeyIdentifier', False, 'hash', + subject=ca) + ]) + + ca.add_extensions([ + OpenSSL.crypto.X509Extension( + 'authorityKeyIdentifier', + False, + 'issuer:always,keyid:always', + issuer=ca + ) + ]) + ca.sign(key, 'sha1') + + + ca_key = open( + '{0}/{1}/{2}_ca_cert.key'.format( + _cert_base_path(), + ca_name, + ca_name + ), + 'w' + ) + ca_key.write( + OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + ) + ca_key.close() + + ca_crt = open( + '{0}/{1}/{2}_ca_cert.crt'.format( + _cert_base_path(), + ca_name, + ca_name + ), + 'w' + ) + ca_crt.write( + OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca) + ) + ca_crt.close() + + _write_cert_to_database(ca_name, ca) + + return ('Created CA "{0}", certificate is located at ' + '"{1}/{2}/{3}_ca_cert.crt"').format( + ca_name, + _cert_base_path(), + ca_name, + ca_name + ) + + +def create_csr( + ca_name, + bits=2048, + CN='localhost', + C='US', + ST='Utah', + L='Salt Lake City', + O='Salt Stack', + OU=None, + emailAddress='xyz@pdq.net'): + ''' + Create a Certificate Signing Request (CSR) for a + particular Certificate Authority (CA) + + ca_name + name of the CA + bits + number of RSA key bits, default is 2048 + CN + common name in the request, default is "localhost" + C + country, default is "US" + ST + state, default is "Utah" + L + locality, default is "Centerville", the city where SaltStack originated + O + organization, default is "Salt Stack" + NOTE: Must the same as CA certificate or an error will be raised + OU + organizational unit, default is None + emailAddress + email address for the request, default is 'xyz@pdq.net' + + Writes out a Certificate Signing Request (CSR) If the file already + exists, the function just returns assuming the CSR already exists. + + If the following values were set: + + ca.cert_base_path='/etc/pki/koji' + ca_name='koji' + CN='test.egavas.org' + + the resulting CSR, and corresponding key, would be written in the + following location: + + /etc/pki/koji/certs/test.egavas.org.csr + /etc/pki/koji/certs/test.egavas.org.key + ''' + + if not _ca_exists(ca_name): + return ('Certificate for CA named "{0}" does not exist, please create ' + 'it first.').format(ca_name) + + if not os.path.exists('{0}/{1}/certs/'.format(_cert_base_path(), ca_name)): + os.makedirs("{0}/{1}/certs/".format(_cert_base_path(), ca_name)) + + if os.path.exists('{0}/{1}/certs/{2}.csr'.format( + _cert_base_path(), + ca_name, + CN) + ): + return 'Certificate Request "{0}" already exists'.format(ca_name) + + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) + + req = OpenSSL.crypto.X509Req() + + req.get_subject().C = C + req.get_subject().ST = ST + req.get_subject().L = L + req.get_subject().O = O + if OU: + req.get_subject().OU = OU + req.get_subject().CN = CN + req.get_subject().emailAddress = emailAddress + req.set_pubkey(key) + req.sign(key, 'sha1') + + # Write private key and request + priv_key = open( + '{0}/{1}/certs/{2}.key'.format(_cert_base_path(), ca_name, CN), + 'w+' + ) + priv_key.write( + OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) + ) + priv_key.close() + + csr = open( + '{0}/{1}/certs/{2}.csr'.format(_cert_base_path(), ca_name, CN), + 'w+' + ) + csr.write( + OpenSSL.crypto.dump_certificate_request( + OpenSSL.crypto.FILETYPE_PEM, + req + ) + ) + csr.close() + + return ('Created CSR for "{0}", request is located at ' + '"{1}/{2}/certs/{3}.csr"').format( + ca_name, + _cert_base_path(), + ca_name, + CN + ) + + +def create_self_signed_cert(bits=2048): + ''' + Create a Self-Signed Certificate (CERT) -- Not yet implemented + ''' + pass + + +def create_ca_signed_cert(ca_name, CN, days=365): + ''' + Create a Certificate (CERT) signed by a + particular Certificate Authority (CA) + + ca_name + name of the CA + CN + common name matching the the certificate signing request + days + number of days certficate is valid, default is 365 (1 year) + + Writes out a Certificate (CERT) If the file already + exists, the function just returns assuming the CERT already exists. + + The CN *must* match an existing CSR generated by create_csr. If it + does not, this method does nothing. + ''' + if os.path.exists( + '{0}/{1}/{2}.crt'.format(_cert_base_path(), ca_name, CN) + ): + return 'Certificate "{0}" already exists'.format(ca_name) + + try: + ca_cert = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + open('{0}/{1}/{2}_ca_cert.crt'.format( + _cert_base_path(), + ca_name, ca_name + )).read() + ) + ca_key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + open('{0}/{1}/{2}_ca_cert.key'.format( + _cert_base_path(), + ca_name, + ca_name + )).read() + ) + except IOError: + return 'There is no CA named "{0}"'.format(ca_name) + + try: + req = OpenSSL.crypto.load_certificate_request( + OpenSSL.crypto.FILETYPE_PEM, + open('{0}/{1}/certs/{2}.csr'.format( + _cert_base_path(), + ca_name, + CN + )).read() + ) + except IOError: + return 'There is no CSR that matches the CN "{0}"'.format(CN) + + cert = OpenSSL.crypto.X509() + cert.set_subject(req.get_subject()) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(days * 24 * 60 * 60) + cert.set_serial_number(_new_serial(ca_name, CN)) + cert.set_issuer(ca_cert.get_subject()) + cert.set_pubkey(req.get_pubkey()) + cert.sign(ca_key, 'sha1') + + crt = open('{0}/{1}/certs/{2}.crt'.format( + _cert_base_path(), + ca_name, + CN + ), 'w+') + crt.write( + OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_PEM, + cert + ) + ) + crt.close() + + _write_cert_to_database(ca_name, cert) + + return ('Created Certificate for "{0}", located at ' + '"{1}/{2}/certs/{3}.crt"').format( + ca_name, + _cert_base_path(), + ca_name, + CN + ) + + +def create_pkcs12(ca_name, CN, passphrase=''): + ''' + Create a PKCS#12 browser certificate for a particular Certificate (CN) + + ca_name + name of the CA + CN + common name matching the the certificate signing request + passphrase + used to unlock the PKCS#12 certificate when loaded into the browser + ''' + if os.path.exists( + '{0}/{1}/certs/{2}.p12'.format( + _cert_base_path(), + ca_name, + CN) + ): + return 'Certificate "{0}" already exists'.format(CN) + + try: + ca_cert = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + open('{0}/{1}/{2}_ca_cert.crt'.format( + _cert_base_path(), + ca_name, + ca_name + )).read() + ) + except IOError: + return 'There is no CA named "{0}"'.format(ca_name) + + try: + cert = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + open('{0}/{1}/certs/{2}.crt'.format( + _cert_base_path(), + ca_name, + CN + )).read() + ) + key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + open('{0}/{1}/certs/{2}.key'.format( + _cert_base_path(), + ca_name, + CN + )).read() + ) + except IOError: + return 'There is no certificate that matches the CN "{0}"'.format(CN) + + pkcs12 = OpenSSL.crypto.PKCS12() + + pkcs12.set_certificate(cert) + pkcs12.set_ca_certificates([ca_cert]) + pkcs12.set_privatekey(key) + + with open('{0}/{1}/certs/{2}.p12'.format( + _cert_base_path(), + ca_name, + CN + ), 'w') as f: + f.write(pkcs12.export(passphrase=passphrase)) + + return ('Created PKCS#12 Certificate for "{0}", located at ' + '"{1}/{2}/certs/{3}.p12"').format( + CN, + _cert_base_path(), + ca_name, + CN + ) + +if __name__ == '__main__': + create_ca( + 'koji', + days=365, + CN='localhost', + C='US', + ST='Utah', + L='Salt Lake City', + O='Salt Stack', + emailAddress='salt@saltstack.org' + ) + create_csr( + 'koji', + CN='test_system', + C="US", + ST="Utah", + L="Centerville", + O="Salt Stack", + OU=None, + emailAddress='test_system@saltstack.org' + ) + create_ca_signed_cert('koji', 'test_system') + create_pkcs12('koji', 'test_system', passphrase='test') diff --git a/salt/modules/cassandra.py b/salt/modules/cassandra.py index 8891fd0cc0..65159c20d9 100644 --- a/salt/modules/cassandra.py +++ b/salt/modules/cassandra.py @@ -15,10 +15,17 @@ REQUIREMENT 2: The python module, 'pycassa', also needs to be installed on the minion. ''' - +# Import Python libs import logging log = logging.getLogger(__name__) +load = False +try: + from pycassa.system_manager import SystemManager + load = True +except ImportError: + pass + __outputter__ = { 'compactionstats': 'txt', 'tpstats': 'txt', @@ -29,15 +36,20 @@ __outputter__ = { def __virtual__(): + ''' + Only load if pycassa is available and the system is configured + ''' global nt global host global thrift_port + if not load: + return False + try: nt = __pillar__['cassandra.nodetool'] host = __pillar__['cassandra.host'] thrift_port = str(__pillar__['cassandra.thrift_port']) - from pycassa.system_manager import SystemManager except ImportError: #log.info('Module failed to load: pycassa is not installed') return False @@ -54,15 +66,14 @@ def _nodetool(cmd): Internal cassandra nodetool wrapper. Some functions are not available via pycassa so we must rely on nodetool. ''' - return __salt__['cmd.run_stdout'](nt + ' -h ' + host + ' ' + cmd) + return __salt__['cmd.run_stdout']('{0} -h {1} {2}'.format(nt, host, cmd)) def _sys_mgr(): ''' Return a pycassa system manager connection object ''' - from pycassa.system_manager import SystemManager - return SystemManager(host + ':' + thrift_port) + return SystemManager('{0}:{1}'.format(host, thrift_port)) def compactionstats(): diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 7c9ef52000..8cb4d071e1 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -4,12 +4,16 @@ A module for shelling out Keep in mind that this module is insecure, in that it can give whomever has access to the master root execution access to all salt minions ''' - +# Import Python libs import pipes import logging import os +import shutil import subprocess import tempfile +import sys + +# Import Salt libs import salt.utils from salt.exceptions import CommandExecutionError from salt.grains.extra import shell as shell_grain @@ -58,7 +62,14 @@ def _run(cmd, if not cwd: cwd = os.path.expanduser('~{0}'.format('' if not runas else runas)) - if 'os' in os.environ and not os.environ['os'].startswith('Windows'): + # make sure we can access the cwd + # when run from sudo or another environment where the euid is + # changed ~ will expand to the home of the original uid and + # the euid might not have access to it. See issue #1844 + if not os.access(cwd, os.R_OK): + cwd = '/' + + if not sys.platform.startswith('win'): if not os.path.isfile(shell) or not os.access(shell, os.X_OK): msg = 'The shell {0} is not available'.format(shell) raise CommandExecutionError(msg) @@ -78,7 +89,7 @@ def _run(cmd, # Save the original command before munging it orig_cmd = cmd try: - p = pwd.getpwnam(runas) + pwd.getpwnam(runas) except KeyError: msg = 'User \'{0}\' is not available'.format(runas) raise CommandExecutionError(msg) @@ -108,8 +119,11 @@ def _run(cmd, 'env': run_env, 'stdout': stdout, 'stderr':stderr} - if not os.environ.get('os', '').startswith('Windows'): + if not sys.platform.startswith('win'): + # close_fds is not supported on Windows platforms if you redirect + # stdin/stdout/stderr kwargs['executable'] = shell + kwargs['close_fds'] = True # This is where the magic happens proc = subprocess.Popen(cmd, **kwargs) @@ -129,10 +143,10 @@ def _run(cmd, out, err = proc.communicate() if rstrip: - if out: + if out is not None: out = out.rstrip() # None lacks a rstrip() method - if err: + if err is not None: err = err.rstrip() ret['stdout'] = out @@ -237,6 +251,7 @@ def retcode(cmd, cwd=None, runas=None, shell=DEFAULT_SHELL, env=()): def script( source, + args=None, cwd=None, runas=None, shell=DEFAULT_SHELL, @@ -252,20 +267,24 @@ def script( programming language. The script can also be formated as a template, the default is jinja. + Arguments for the script can be specified as well. CLI Example:: salt '*' cmd.script salt://scripts/runme.sh + salt '*' cmd.script salt://scripts/runme.sh 'arg1 arg2 "arg 3"' ''' fd_, path = tempfile.mkstemp() os.close(fd_) if template: - fn_ = __salt__['cp.get_template'](source, path, template, env, **kwargs) + __salt__['cp.get_template'](source, path, template, env, **kwargs) else: fn_ = __salt__['cp.cache_file'](source, env) + shutil.copyfile(fn_, path) os.chmod(path, 320) + os.chown(path, __salt__['file.user_to_uid'](runas), -1) ret = _run( - path, + path +' '+ args if args else path, cwd=cwd, quiet=kwargs.get('quiet', False), runas=runas, diff --git a/salt/modules/cp.py b/salt/modules/cp.py index 5997e3cdde..ce7aad7c2a 100644 --- a/salt/modules/cp.py +++ b/salt/modules/cp.py @@ -3,11 +3,14 @@ Minion side functions for salt-cp ''' # Import python libs import os +import logging # Import salt libs import salt.minion import salt.fileclient +log = logging.getLogger(__name__) + def recv(files, dest): ''' @@ -197,6 +200,18 @@ def list_master(env='base'): return client.file_list(env) +def list_master_dirs(env='base'): + ''' + List all of the directories stored on the master + + CLI Exmaple:: + + salt '*' cp.list_master_dirs + ''' + client = salt.fileclient.get_file_client(__opts__) + return client.dir_list(env) + + def list_minion(env='base'): ''' List all of the files cached on the minion diff --git a/salt/modules/cron.py b/salt/modules/cron.py index 835a8e2c07..16ebfcb782 100644 --- a/salt/modules/cron.py +++ b/salt/modules/cron.py @@ -144,15 +144,14 @@ def set_special(user, special, cmd): salt '*' cron.set_special @hourly 'echo foobar' ''' lst = list_tab(user) - for cron in lst['crons']: - for spec in lst['special']: - if special == cron['special'] and cmd == cron['cmd']: - return 'present' - spec = {'special': special, + for cron in lst['special']: + if special == cron['spec'] and cmd == cron['cmd']: + return 'present' + spec = {'spec': special, 'cmd': cmd} lst['special'].append(spec) comdat = _write_cron(user, _render_tab(lst)) - if not comdat['retcode']: + if comdat['retcode']: # Failed to commit, return the error return comdat['stderr'] return 'new' diff --git a/salt/modules/debconfmod.py b/salt/modules/debconfmod.py index 9fe6547c9a..c913284f38 100644 --- a/salt/modules/debconfmod.py +++ b/salt/modules/debconfmod.py @@ -1,13 +1,16 @@ ''' Support for Debconf ''' - +# Import Salt libs import os import re import tempfile def _unpack_lines(out): + ''' + Unpack the debconf lines + ''' rexp = ('(?ms)' '^(?P[^#]\S+)[\t ]+' '(?P\S+)[\t ]+' @@ -21,7 +24,6 @@ def __virtual__(): ''' Confirm this module is on a Debian based system ''' - return 'debconf' if __grains__['os'] in ['Debian', 'Ubuntu'] else False @@ -73,9 +75,12 @@ def show(name): return result def _set_file(path): + ''' + Execute the set selections command for debconf + ''' cmd = 'debconf-set-selections {0}'.format(path) - out = __salt__['cmd.run_stdout'](cmd) + __salt__['cmd.run_stdout'](cmd) def set(package, question, type, value, *extra): ''' diff --git a/salt/modules/debian_service.py b/salt/modules/debian_service.py index f0a372a2c0..c008bde647 100644 --- a/salt/modules/debian_service.py +++ b/salt/modules/debian_service.py @@ -100,6 +100,16 @@ def restart(name): cmd = 'service {0} restart'.format(name) return not __salt__['cmd.retcode'](cmd) +def reload(name): + ''' + Reload the named service + + CLI Example:: + + salt '*' service.reload + ''' + cmd = 'service {0} reload'.format(name) + return not __salt__['cmd.retcode'](cmd) def status(name, sig=None): ''' diff --git a/salt/modules/disk.py b/salt/modules/disk.py index 0fca70820a..dedee987e3 100644 --- a/salt/modules/disk.py +++ b/salt/modules/disk.py @@ -33,7 +33,8 @@ def usage(args=None): cmd = 'df -kP' else: cmd = 'df' - cmd = cmd + ' -' + args + if args: + cmd = cmd + ' -' + args ret = {} out = __salt__['cmd.run'](cmd).split('\n') for line in out: @@ -64,7 +65,8 @@ def inodeusage(args=None): salt '*' disk.inodeusage ''' cmd = 'df -i' - cmd = cmd + ' -' + args + if args is not None: + cmd = cmd + ' -' + args ret = {} out = __salt__['cmd.run'](cmd).split('\n') for line in out: diff --git a/salt/modules/django.py b/salt/modules/djangomod.py similarity index 99% rename from salt/modules/django.py rename to salt/modules/djangomod.py index 6bffe53fcf..d61c570953 100644 --- a/salt/modules/django.py +++ b/salt/modules/djangomod.py @@ -4,6 +4,8 @@ Manage Django sites import os +def __virtual__(): + return 'django' def _get_django_admin(bin_env): ''' diff --git a/salt/modules/event.py b/salt/modules/event.py index 0cb79d2cfa..f0ae4324f6 100644 --- a/salt/modules/event.py +++ b/salt/modules/event.py @@ -35,5 +35,4 @@ def fire(data, tag): salt '*' event.fire 'stuff to be in the event' 'tag' ''' - esock = salt.utils.event.MinionEvent(__opts__['sock_dir']) - return esock.fire_event(data, tag) + return salt.utils.event.MinionEvent(**__opts__).fire_event(data, tag) diff --git a/salt/modules/file.py b/salt/modules/file.py index 580ad8dffc..5a60377337 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -10,10 +10,11 @@ data import os import re import time -import hashlib import shutil import stat import sys +import getpass +import hashlib import fnmatch try: import grp @@ -23,6 +24,7 @@ except ImportError: # Import salt libs import salt.utils.find +from salt.utils.filebuffer import BufferedReader from salt.exceptions import CommandExecutionError, SaltInvocationError def __virtual__(): @@ -118,6 +120,8 @@ def user_to_uid(user): salt '*' file.user_to_uid root ''' + if not user: + user = getpass.getuser() try: return pwd.getpwnam(user).pw_uid except KeyError: @@ -242,9 +246,9 @@ def get_sum(path, form='md5'): with open(path, 'rb') as f: return getattr(hashlib, form)(f.read()).hexdigest() except (IOError, OSError) as e: - return 'File Error: %s' % (str(e)) + return 'File Error: {0}'.format(e) except AttributeError as e: - return 'Hash ' + form + ' not supported' + return 'Hash {0} not supported'.format(form) except NameError as e: return 'Hashlib unavailable - please fix your python install' except Exception as e: @@ -512,9 +516,9 @@ def contains(path, text): return False try: - with open(path, 'r') as fp_: - for line in fp_: - if text.strip() == line.strip(): + with BufferedReader(path) as br: + for chunk in br: + if text.strip() == chunk.strip(): return True return False except (IOError, OSError): @@ -534,9 +538,11 @@ def contains_regex(path, regex, lchar=''): return False try: - with open(path, 'r') as fp_: - for line in fp_: - if re.search(regex, line.lstrip(lchar)): + with BufferedReader(path) as br: + for chunk in br: + if lchar: + chunk = chunk.lstrip(lchar) + if re.search(regex, chunk): return True return False except (IOError, OSError): @@ -555,12 +561,11 @@ def contains_glob(path, glob): return False try: - with open(path, 'r') as fp_: - data = fp_.read() - if fnmatch.fnmatch(data, glob): - return True - else: - return False + with BufferedReader(path) as br: + for chunk in br: + if fnmatch.fnmatch(chunk, glob): + return True + return False except (IOError, OSError): return False @@ -608,21 +613,23 @@ def touch(name, atime=None, mtime=None): if mtime and mtime.isdigit(): mtime = int(mtime) try: - with open(name, 'a'): - if not atime and not mtime: - times = None - elif not mtime and atime: - times = (atime, time.time()) - elif not atime and mtime: - times = (time.time(), mtime) - else: - times = (atime, mtime) - os.utime(name, times) + if not os.path.exists(name): + open(name, 'a') + + if not atime and not mtime: + times = None + elif not mtime and atime: + times = (atime, time.time()) + elif not atime and mtime: + times = (time.time(), mtime) + else: + times = (atime, mtime) + os.utime(name, times) + except TypeError as exc: - msg = 'atime and mtime must be integers' - raise SaltInvocationError(msg) + raise SaltInvocationError('atime and mtime must be integers') except (IOError, OSError) as exc: - return False + raise CommandExecutionError(exc.strerror) return os.path.exists(name) @@ -731,7 +738,7 @@ def get_selinux_context(path): Get an SELinux context from a given path CLI Example:: - + salt '*' selinux.get_context /etc/hosts ''' out = __salt__['cmd.run']('ls -Z {0}'.format(path)) diff --git a/salt/modules/freebsdkmod.py b/salt/modules/freebsdkmod.py index 88836284b8..f71514f0da 100644 --- a/salt/modules/freebsdkmod.py +++ b/salt/modules/freebsdkmod.py @@ -105,7 +105,7 @@ def load(mod): salt '*' kmod.load kvm ''' pre_mods = lsmod() - data = __salt__['cmd.run_all']('kldload {0}'.format(mod)) + __salt__['cmd.run_all']('kldload {0}'.format(mod)) post_mods = lsmod() return _new_mods(pre_mods, post_mods) @@ -119,6 +119,6 @@ def remove(mod): salt '*' kmod.remove kvm ''' pre_mods = lsmod() - data = __salt__['cmd.run_all']('kldunload {0}'.format(mod)) + __salt__['cmd.run_all']('kldunload {0}'.format(mod)) post_mods = lsmod() return _rm_mods(pre_mods, post_mods) diff --git a/salt/modules/freebsdpkg.py b/salt/modules/freebsdpkg.py index 436c5ee1fc..667e66a241 100644 --- a/salt/modules/freebsdpkg.py +++ b/salt/modules/freebsdpkg.py @@ -4,6 +4,7 @@ Package support for FreeBSD import os + def _check_pkgng(): ''' Looks to see if pkgng is being used by checking if database exists @@ -19,12 +20,13 @@ def search(pkg_name): CLI Example:: - salt '*' pkg.pkgng_search 'mysql-server' + salt '*' pkg.search 'mysql-server' ''' if _check_pkgng(): res = __salt__['cmd.run']('pkg search {0}'.format(pkg_name)) - res = [ x for x in res.split('\n') ] - return { "Results" : res } + res = [x for x in res.split('\n')] + return {"Results": res} + def __virtual__(): ''' @@ -72,8 +74,8 @@ def version(name): def refresh_db(): ''' - Use pkg update to get latest repo.txz when using pkgng, else update the - ports tree with portsnap otherwise. If the ports tree does not exist it + Use pkg update to get latest repo.txz when using pkgng, else update the + ports tree with portsnap otherwise. If the ports tree does not exist it will be downloaded and set up. CLI Example:: @@ -114,7 +116,7 @@ def list_pkgs(): return ret -def install(name, *args, **kwargs): +def install(name, refresh=False, repo='', **kwargs): ''' Install the passed package @@ -127,12 +129,19 @@ def install(name, *args, **kwargs): salt '*' pkg.install ''' - if _check_pkgng: + env = () + if _check_pkgng(): pkg_command = 'pkg install -y' + if not refresh: + pkg_command += ' -L' + if repo: + env = (('PACKAGESITE', repo),) else: pkg_command = 'pkg_add -r' + if repo: + env = (('PACKAGEROOT', repo),) old = list_pkgs() - __salt__['cmd.retcode']('%s {0}'.format(name) % pkg_command) + __salt__['cmd.retcode']('{0} {1}'.format(pkg_command, name), env=env) new = list_pkgs() pkgs = {} for npkg in new: @@ -154,7 +163,7 @@ def install(name, *args, **kwargs): def upgrade(): ''' - Run a full system upgrade, a ``freebsd-update fetch install`` + Run pkg upgrade, if pkgng used. Otherwise do nothing Return a dict containing the new package names and versions:: @@ -165,8 +174,13 @@ def upgrade(): salt '*' pkg.upgrade ''' + + if not _check_pkgng(): + # There is not easy way to upgrade packages with old package system + return {} + old = list_pkgs() - __salt__['cmd.retcode']('freebsd-update fetch install') + __salt__['cmd.retcode']('pkg upgrade -y') new = list_pkgs() pkgs = {} for npkg in new: @@ -203,7 +217,7 @@ def remove(name): pkg_command = 'pkg delete -y' else: pkg_command - 'pkg_delete' - __salt__['cmd.retcode']('%s {0}'.format(name)% pkg_command) + __salt__['cmd.retcode']('{0} {1}'.format(pkg_command, name)) new = list_pkgs() return _list_removed(old, new) @@ -220,6 +234,7 @@ def purge(name): ''' return remove(name) + def rehash(): ''' Recomputes internal hash table for the PATH variable. @@ -230,6 +245,6 @@ def rehash(): salt '*' pkg.rehash ''' - shell = __salt__['cmd.run']('echo $SHELL').split('/') - if shell[len(shell)-1] in ["csh","tcsh"]: + shell = __salt__['cmd.run']('echo $SHELL').split('/') + if shell[len(shell)-1] in ["csh", "tcsh"]: __salt__['cmd.run']('rehash') diff --git a/salt/modules/freebsdservice.py b/salt/modules/freebsdservice.py index 5476ed0540..4218468d6e 100644 --- a/salt/modules/freebsdservice.py +++ b/salt/modules/freebsdservice.py @@ -1,8 +1,10 @@ ''' The service module for FreeBSD ''' - +# Import Python libs import os +# Import Salt libs +import salt.utils def __virtual__(): @@ -104,6 +106,8 @@ def restart(name): salt '*' service.restart ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) cmd = 'service {0} onerestart'.format(name) return not __salt__['cmd.retcode'](cmd) @@ -118,9 +122,6 @@ def status(name, sig=None): salt '*' service.status [service signature] ''' - sig = name if not sig else sig - cmd = "{0[ps]} | grep {1} | grep -v grep | awk '{{print $2}}'".format( - __grains__, sig) - return __salt__['cmd.run'](cmd).strip() + return __salt__['status.pid'](sig if sig else name) diff --git a/salt/modules/gentoo_service.py b/salt/modules/gentoo_service.py index f85f115cef..e542cc9b0d 100644 --- a/salt/modules/gentoo_service.py +++ b/salt/modules/gentoo_service.py @@ -2,7 +2,8 @@ Top level package command wrapper, used to translate the os detected by the grains to the correct service manager ''' - +# Import Salt Libs +import salt.utils def __virtual__(): ''' @@ -95,6 +96,8 @@ def restart(name): salt '*' service.restart ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) cmd = '/etc/init.d/{0} restart'.format(name) return not __salt__['cmd.retcode'](cmd) @@ -109,10 +112,7 @@ def status(name, sig=None): salt '*' service.status [service signature] ''' - sig = name if not sig else sig - cmd = "{0[ps]} | grep {1} | grep -v grep | awk '{{print $2}}'".format( - __grains__, sig) - return __salt__['cmd.run'](cmd).strip() + return __salt__['status.pid'](sig if sig else name) def enable(name): ''' diff --git a/salt/modules/grub.py b/salt/modules/grub.py index d1fb8733ed..9a9bc7bfa8 100644 --- a/salt/modules/grub.py +++ b/salt/modules/grub.py @@ -17,7 +17,7 @@ def _detect_conf(): ''' GRUB conf location differs depending on distro ''' - conf = ('CentOS', 'Scientific', 'RedHat', 'Fedora') + conf = ('CentOS', 'Scientific', 'RedHat', 'Fedora', 'CloudLinux') menu = ('Ubuntu', 'Debian', 'Arch') if __grains__['os'] in conf: return '/boot/grub/grub.conf' diff --git a/salt/modules/hosts.py b/salt/modules/hosts.py index 2fdae0b440..0db92f87e2 100644 --- a/salt/modules/hosts.py +++ b/salt/modules/hosts.py @@ -119,7 +119,7 @@ def set_host(ip, alias): if not ovr: # make sure there is a newline if lines and not lines[-1].endswith(('\n', '\r')): - lines[-1] = '%s\n' % lines[-1] + lines[-1] = '{0}\n'.format(lines[-1]) line = ip + '\t\t' + alias + '\n' lines.append(line) open(hfn, 'w+').writelines(lines) @@ -195,7 +195,7 @@ def add_host(ip, alias): if not ovr: # make sure there is a newline if lines and not lines[-1].endswith(('\n', '\r')): - lines[-1] = '%s\n' % lines[-1] + lines[-1] = '{0}\n'.format(lines[-1]) line = ip + '\t\t' + alias + '\n' lines.append(line) open(hfn, 'w+').writelines(lines) diff --git a/salt/modules/kmod.py b/salt/modules/kmod.py index 5904deb39a..6b8536b036 100644 --- a/salt/modules/kmod.py +++ b/salt/modules/kmod.py @@ -49,12 +49,11 @@ def available(): salt '*' kmod.available ''' ret = [] - for path in __salt__['cmd.run']('modprobe -l').split('\n'): - bpath = os.path.basename(path) - comps = bpath.split('.') - if 'ko' in comps: - # This is a kernel module, return it without the .ko extension - ret.append('.'.join(comps[:comps.index('ko')])) + mod_dir = os.path.join('/lib/modules/', os.uname()[2], 'kernel') + for root, dirs, files in os.walk(mod_dir): + for fn_ in files: + if '.ko' in fn_: + ret.append(fn_[:fn_.index('.ko')]) return sorted(list(ret)) @@ -120,7 +119,7 @@ def load(mod): salt '*' kmod.load kvm ''' pre_mods = lsmod() - data = __salt__['cmd.run_all']('modprobe {0}'.format(mod)) + __salt__['cmd.run_all']('modprobe {0}'.format(mod)) post_mods = lsmod() return _new_mods(pre_mods, post_mods) @@ -134,6 +133,6 @@ def remove(mod): salt '*' kmod.remove kvm ''' pre_mods = lsmod() - data = __salt__['cmd.run_all']('modprobe -r {0}'.format(mod)) + __salt__['cmd.run_all']('modprobe -r {0}'.format(mod)) post_mods = lsmod() return _rm_mods(pre_mods, post_mods) diff --git a/salt/modules/mdadm.py b/salt/modules/mdadm.py index b6e36a5f26..4871db0b11 100644 --- a/salt/modules/mdadm.py +++ b/salt/modules/mdadm.py @@ -49,7 +49,7 @@ def detail(device='/dev/md0'): ''' ret = {} ret['members'] = {} - cmd = 'mdadm --detail %s' % device + cmd = 'mdadm --detail {0}'.format(device) for line in __salt__['cmd.run_stdout'](cmd).split('\n'): if line.startswith(device): continue diff --git a/salt/modules/mongodb.py b/salt/modules/mongodb.py new file mode 100644 index 0000000000..aa19d2ae9a --- /dev/null +++ b/salt/modules/mongodb.py @@ -0,0 +1,166 @@ +''' +Module to provide MongoDB functionality to Salt + +This module uses PyMongo, and accepts configuration details as parameters +as well as configuration settings: + + mongodb.host: 'localhost' + mongodb.port: '27017' + mongodb.user: '' + mongodb.password: '' + +This data can also be passed into pillar. Options passed into opts will +overwrite options passed into pillar. +''' + +import logging + +# Import third party libs +try: + import pymongo + has_mongodb = True +except ImportError: + has_mongodb = False + +log = logging.getLogger(__name__) +__opts__ = {} + +def __virtual__(): + ''' + Only load this module if pymongo is installed + ''' + if has_mongodb: + return 'mongodb' + else: + return False + +def _connect(user=None, password=None, host=None, port=None, database="admin"): + ''' + Returns a tuple of (user, host, port) with config, pillar, or default + values assigned to missing values. + ''' + if not user: + user = __opts__.get('mongodb.user') or __pillar__.get('mongodb.user') + if not password: + password = __opts__.get('mongodb.password') or __pillar__.get('mongodb.password') + if not host: + host = __opts__.get('mongodb.host') or __pillar__.get('mongodb.host') + if not port: + port = __opts__.get('mongodb.port') or __pillar__.get('mongodb.port') + + try: + conn = pymongo.connection.Connection(host=host, port=port) + db = pymongo.database.Database(conn, database) + if user and password: + db.authenticate(user, password) + except pymongo.errors.PyMongoError: + log.error("Error connecting to database {0}".format(database.message)) + return False + + return conn + +def db_list(user=None, password=None, host=None, port=None): + ''' + List all Mongodb databases + ''' + conn = _connect(user, password, host, port) + + try: + log.info("Listing databases") + return conn.database_names() + except pymongo.errors.PyMongoError as e: + log.error(e) + return e.message + +def db_exists(name, user=None, password=None, host=None, port=None, database="admin"): + ''' + Checks if a database exists in Mongodb + ''' + dbs = db_list(user, password, host, port) + for db in dbs: + if name == db: + return True + + return False + +def db_remove(name, user=None, password=None, host=None, port=None): + ''' + Remove a Mongodb database + ''' + conn = _connect(user, password, host, port) + + try: + log.info("Removing database {0}".format(name)) + conn.drop_database(name) + except pymongo.errors.PyMongoError as e: + log.error("Removing database {0} failed with error: {1}".format( + name, e.message)) + return e.message + + return True + +def user_list(user=None, password=None, host=None, port=None, database="admin"): + ''' + List users of a Mongodb database + ''' + conn = _connect(user, password, host, port) + + try: + log.info("Listing users") + db = pymongo.database.Database(conn, database) + + output = [] + + for user in db.system.users.find(): + output.append([("user", user['user']), ("readOnly", user['readOnly'])]) + + return output + + except pymongo.errors.PyMongoError as e: + log.error("Listing users failed with error: {0}".format(e.message)) + return e.message + +def user_exists(name, user=None, password=None, host=None, port=None, database="admin"): + ''' + Checks if a user exists in Mongodb + ''' + users = user_list(user, password, host, port, database) + for user in users: + if name == dict(user).get('user'): + return True + + return False + +def user_create(name, passwd, user=None, password=None, host=None, port=None, database="admin"): + ''' + Create a Mongodb user + ''' + conn = _connect(user, password, host, port) + + try: + log.info("Creating user {0}".format(name)) + db = pymongo.database.Database(conn, database) + db.add_user(name, passwd) + except pymongo.errors.PyMongoError as e: + log.error("Creating database {0} failed with error: {1}".format( + name, e.message)) + return e.message + + return True + +def user_remove(name, user=None, password=None, host=None, port=None, database="admin"): + ''' + Remove a Mongodb user + ''' + conn = _connect(user, password, host, port) + + try: + log.info("Removing user {0}".format(name)) + db = pymongo.database.Database(conn, database) + db.remove_user(name) + except pymongo.errors.PyMongoError as e: + log.error("Creating database {0} failed with error: {1}".format( + name, e.message)) + return e.message + + return True diff --git a/salt/modules/monit.py b/salt/modules/monit.py index 4eb8c53632..99eb6af38f 100644 --- a/salt/modules/monit.py +++ b/salt/modules/monit.py @@ -3,7 +3,6 @@ Monit service module. This module will create a monit type service watcher. ''' -import os def start(name): ''' diff --git a/salt/modules/mount.py b/salt/modules/mount.py index 7f971bd1ee..8b941eb994 100644 --- a/salt/modules/mount.py +++ b/salt/modules/mount.py @@ -256,6 +256,24 @@ def remount(name, device, mkmnt=False, fstype='', opts='defaults'): return mount(name, device, mkmnt, fstype, opts) +def umount(name): + ''' + Attempt to unmount a device by specifying the directory it is mounted on + + CLI Example:: + + salt '*' mount.umount /mnt/foo + ''' + mnts = active() + if name not in mnts: + return "{0} does not have anything mounted".format(name) + + cmd = 'umount {0}'.format(name) + out = __salt__['cmd.run_all'](cmd) + if out['retcode']: + return out['stderr'] + return True + def is_fuse_exec(cmd): ''' Returns true if the command passed is a fuse mountable application. diff --git a/salt/modules/mysql.py b/salt/modules/mysql.py index af7bca88f8..46228788f2 100644 --- a/salt/modules/mysql.py +++ b/salt/modules/mysql.py @@ -24,6 +24,7 @@ Required python modules: MySQLdb # Import Python libs import time import logging +import re # Import third party libs try: @@ -36,6 +37,7 @@ except ImportError: log = logging.getLogger(__name__) __opts__ = {} + def __virtual__(): ''' Only load this module if the mysql config is set @@ -52,33 +54,33 @@ def __virtual__(): def __check_table(name, table): db = connect() cur = db.cursor(MySQLdb.cursors.DictCursor) - query = "CHECK TABLE `%s`.`%s`" % (name, table, ) - log.debug("Doing query: {0}".format(query,)) - cur.execute( query ) + query = 'CHECK TABLE `{0}`.`{1}`'.format(name, table) + log.debug('Doing query: {0}'.format(query)) + cur.execute(query) results = cur.fetchall() - log.debug( results ) + log.debug(results) return results def __repair_table(name, table): db = connect() cur = db.cursor(MySQLdb.cursors.DictCursor) - query = "REPAIR TABLE `%s`.`%s`" % (name, table, ) - log.debug("Doing query: {0}".format(query,)) - cur.execute( query ) + query = 'REPAIR TABLE `{0}`.`{1}`'.format(name, table) + log.debug('Doing query: {0}'.format(query)) + cur.execute(query) results = cur.fetchall() - log.debug( results ) + log.debug(results) return results def __optimize_table(name, table): db = connect() cur = db.cursor(MySQLdb.cursors.DictCursor) - query = "OPTIMIZE TABLE `%s`.`%s`" % (name, table, ) - log.debug("Doing query: {0}".format(query,)) - cur.execute( query ) + query = 'OPTIMIZE TABLE `{0}`.`{1}`'.format(name, table) + log.debug('Doing query: {0}'.format(query)) + cur.execute(query) results = cur.fetchall() - log.debug( results ) + log.debug(results) return results @@ -87,6 +89,7 @@ def connect(**kwargs): wrap authentication credentials here ''' connargs = dict() + def _connarg(name, key=None): ''' Add key to connargs, only if name exists in our @@ -188,7 +191,7 @@ def status(): db = connect() cur = db.cursor() cur.execute('SHOW STATUS') - for i in range( cur.rowcount ): + for i in range(cur.rowcount): row = cur.fetchone() ret[row[0]] = row[1] return ret @@ -223,7 +226,7 @@ def slave_lag(): ''' db = connect() cur = db.cursor(MySQLdb.cursors.DictCursor) - cur.execute("show slave status") + cur.execute('show slave status') results = cur.fetchone() if cur.rowcount == 0: # Server is not a slave if master is not defined. Return empty tuple @@ -311,14 +314,14 @@ def db_tables(name): ret = [] db = connect() cur = db.cursor() - query = "SHOW TABLES IN %s" % name - log.debug("Doing query: {0}".format(query,)) + query = 'SHOW TABLES IN {0}'.format(name) + log.debug('Doing query: {0}'.format(query)) - cur.execute( query ) + cur.execute(query) results = cur.fetchall() for table in results: ret.append(table[0]) - log.debug( ret ) + log.debug(ret) return ret @@ -332,10 +335,10 @@ def db_exists(name): ''' db = connect() cur = db.cursor() - query = "SHOW DATABASES LIKE '%s'" % name - log.debug("Doing query: {0}".format(query,)) - cur.execute( query ) - result_set = cur.fetchall() + query = 'SHOW DATABASES LIKE \'{0}\''.format(name) + log.debug('Doing query: {0}'.format(query)) + cur.execute(query) + cur.fetchall() return cur.rowcount == 1 @@ -348,17 +351,17 @@ def db_create(name): salt '*' mysql.db_create 'dbname' ''' # check if db exists - if db_exists( name ): - log.info("DB '{0}' already exists".format(name,)) + if db_exists(name): + log.info('DB \'{0}\' already exists'.format(name)) return False # db doesnt exist, proceed db = connect() cur = db.cursor() - query = "CREATE DATABASE `%s`;" % name - log.debug("Query: {0}".format(query,)) - if cur.execute( query ): - log.info("DB '{0}' created".format(name,)) + query = 'CREATE DATABASE `{0}`;'.format(name) + log.debug('Query: {0}'.format(query)) + if cur.execute(query): + log.info('DB \'{0}\' created'.format(name)) return True return False @@ -372,26 +375,26 @@ def db_remove(name): salt '*' mysql.db_remove 'dbname' ''' # check if db exists - if not db_exists( name ): - log.info("DB '{0}' does not exist".format(name,)) + if not db_exists(name): + log.info('DB \'{0}\' does not exist'.format(name)) return False if name in ('mysql', 'information_scheme'): - log.info("DB '{0}' may not be removed".format(name,)) + log.info('DB \'{0}\' may not be removed'.format(name)) return False # db doesnt exist, proceed db = connect() cur = db.cursor() - query = "DROP DATABASE `%s`;" % name - log.debug("Doing query: {0}".format(query,)) - cur.execute( query ) + query = 'DROP DATABASE `{0}`;'.format(name) + log.debug('Doing query: {0}'.format(query)) + cur.execute(query) - if not db_exists( name ): - log.info("Database '{0}' has been removed".format(name,)) + if not db_exists(name): + log.info('Database \'{0}\' has been removed'.format(name)) return True - log.info("Database '{0}' has not been removed".format(name,)) + log.info('Database \'{0}\' has not been removed'.format(name)) return False @@ -408,7 +411,7 @@ def user_list(): cur = db.cursor(MySQLdb.cursors.DictCursor) cur.execute('SELECT User,Host FROM mysql.user') results = cur.fetchall() - log.debug( results ) + log.debug(results) return results @@ -422,9 +425,10 @@ def user_exists(user, host='localhost'): ''' db = connect() cur = db.cursor() - query = "SELECT User,Host FROM mysql.user WHERE User = '%s' AND Host = '%s'" % (user, host,) - log.debug("Doing query: {0}".format(query,)) - cur.execute( query ) + query = ('SELECT User,Host FROM mysql.user WHERE User = \'{0}\' AND ' + 'Host = \'{0}\''.format(user, host)) + log.debug('Doing query: {0}'.format(query)) + cur.execute(query) return cur.rowcount == 1 @@ -437,12 +441,13 @@ def user_info(user, host='localhost'): salt '*' mysql.user_info root localhost ''' db = connect() - cur = db.cursor (MySQLdb.cursors.DictCursor) - query = "SELECT * FROM mysql.user WHERE User = '%s' AND Host = '%s'" % (user, host,) - log.debug("Query: {0}".format(query,)) - cur.execute( query ) + cur = db.cursor(MySQLdb.cursors.DictCursor) + query = ('SELECT * FROM mysql.user WHERE User = \'{0}\' AND ' + 'Host = \'{1}\''.format(user, host)) + log.debug('Query: {0}'.format(query)) + cur.execute(query) result = cur.fetchone() - log.debug( result ) + log.debug(result) return result @@ -459,26 +464,26 @@ def user_create(user, salt '*' mysql.user_create 'username' 'hostname' password_hash='hash' ''' - if user_exists(user,host): - log.info("User '{0}'@'{1}' already exists".format(user,host,)) - return False + if user_exists(user, host): + log.info('User \'{0}\'@\'{1}\' already exists'.format(user, host)) + return False db = connect() - cur = db.cursor () - query = "CREATE USER '%s'@'%s'" % (user, host,) + cur = db.cursor() + query = 'CREATE USER \'{0}\'@\'{1}\''.format(user, host) if password is not None: - query = query + " IDENTIFIED BY '%s'" % password + query = query + ' IDENTIFIED BY \'{0}\''.format(password) elif password_hash is not None: - query = query + " IDENTIFIED BY PASSWORD '%s'" % password_hash + query = query + ' IDENTIFIED BY PASSWORD \'{0}\''.format(password_hash) - log.debug("Query: {0}".format(query,)) - cur.execute( query ) + log.debug('Query: {0}'.format(query)) + cur.execute(query) if user_exists(user, host): - log.info("User '{0}'@'{1}' has been created".format(user,host,)) + log.info('User \'{0}\'@\'{1}\' has been created'.format(user, host)) return True - log.info("User '{0}'@'{1}' is not created".format(user,host,)) + log.info('User \'{0}\'@\'{1}\' is not created'.format(user, host)) return False @@ -495,23 +500,31 @@ def user_chpass(user, salt '*' mysql.user_chpass frank localhost password_hash='hash' ''' - if password is None or password_hash is None: + if password is None and password_hash is None: log.error('No password provided') return False elif password is not None: - password_sql = "PASSWORD(\"%s\")" % password + password_sql = 'PASSWORD("{0}")'.format(password) elif password_hash is not None: - password_sql = "\"%s\"" % password_hash + password_sql = '"{0}"'.format(password_hash) db = connect() - cur = db.cursor () - query = "UPDATE mysql.user SET password=%s WHERE User='%s' AND Host = '%s';" % (password_sql,user,host,) - log.debug("Query: {0}".format(query,)) - if cur.execute( query ): - log.info("Password for user '{0}'@'{1}' has been changed".format(user,host,)) + cur = db.cursor() + query = ('UPDATE mysql.user SET password={0} WHERE User=\'{1}\' AND ' + 'Host = \'{2}\';'.format(password_sql, user, host)) + log.debug('Query: {0}'.format(query)) + if cur.execute(query): + cur.execute('FLUSH PRIVILEGES;') + log.info( + 'Password for user \'{0}\'@\'{1}\' has been changed'.format( + user, host + ) + ) return True - log.info("Password for user '{0}'@'{1}' is not changed".format(user,host,)) + log.info( + 'Password for user \'{0}\'@\'{1}\' is not changed'.format(user, host) + ) return False @@ -525,21 +538,21 @@ def user_remove(user, salt '*' mysql.user_remove frank localhost ''' db = connect() - cur = db.cursor () - query = "DROP USER '%s'@'%s'" % (user, host,) - log.debug("Query: {0}".format(query,)) - cur.execute( query ) + cur = db.cursor() + query = 'DROP USER \'{0}\'@\'{1}\''.format(user, host) + log.debug('Query: {0}'.format(query)) + cur.execute(query) if not user_exists(user, host): - log.info("User '{0}'@'{1}' has been removed".format(user,host,)) + log.info('User \'{0}\'@\'{1}\' has been removed'.format(user, host)) return True - log.info("User '{0}'@'{1}' has NOT been removed".format(user,host,)) + log.info('User \'{0}\'@\'{1}\' has NOT been removed'.format(user, host)) return False # Maintenance def db_check(name, - table=None): + table=None): ''' Repairs the full database or just a given table @@ -550,12 +563,14 @@ def db_check(name, ret = [] if table is None: # we need to check all tables - tables = db_tables( name ) + tables = db_tables(name) for table in tables: - log.info("Checking table '%s' in db '%s..'".format(name,table,)) - ret.append( __check_table(name, table)) + log.info( + 'Checking table \'{0}\' in db \'{1}..\''.format(name, table) + ) + ret.append(__check_table(name, table)) else: - log.info("Checking table '%s' in db '%s'..".format(name,table,)) + log.info('Checking table \'{0}\' in db \'{1}\'..'.format(name, table)) ret = __check_table(name, table) return ret @@ -572,12 +587,14 @@ def db_repair(name, ret = [] if table is None: # we need to repair all tables - tables = db_tables( name ) + tables = db_tables(name) for table in tables: - log.info("Repairing table '%s' in db '%s..'".format(name,table,)) - ret.append( __repair_table(name, table)) + log.info( + 'Repairing table \'{0}\' in db \'{1}..\''.format(name, table) + ) + ret.append(__repair_table(name, table)) else: - log.info("Repairing table '%s' in db '%s'..".format(name,table,)) + log.info('Repairing table \'{0}\' in db \'{1}\'..'.format(name, table)) ret = __repair_table(name, table) return ret @@ -596,10 +613,14 @@ def db_optimize(name, # we need to optimize all tables tables = db_tables(name) for table in tables: - log.info("Optimizing table '%s' in db '%s..'".format(name,table,)) - ret.append( __optimize_table(name, table)) + log.info( + 'Optimizing table \'{0}\' in db \'{1}..\''.format(name, table) + ) + ret.append(__optimize_table(name, table)) else: - log.info("Optimizing table '%s' in db '%s'..".format(name,table,)) + log.info( + 'Optimizing table \'{0}\' in db \'{1}\'..'.format(name, table) + ) ret = __optimize_table(name, table) return ret @@ -611,8 +632,9 @@ def __grant_generate(grant, host='localhost', grant_option=False, escape=True): - # todo: Re-order the grant so it is according to the SHOW GRANTS for xxx@yyy query (SELECT comes first, etc) - grant = grant.replace(',', ', ').upper() + # TODO: Re-order the grant so it is according to the + # SHOW GRANTS for xxx@yyy query (SELECT comes first, etc) + grant = re.sub(r'\s*,\s*', ', ', grant).upper() # MySQL normalizes ALL to ALL PRIVILEGES, we do the same so that # grant_exists and grant_add ALL work correctly @@ -625,13 +647,15 @@ def __grant_generate(grant, if escape: if db is not '*': - db = "`%s`" % db + db = '`{0}`'.format(db) if table is not '*': - table = "`%s`" % table - query = "GRANT %s ON %s.%s TO '%s'@'%s'" % (grant, db, table, user, host,) + table = '`{0}`'.format(table) + query = 'GRANT {0} ON {1}.{2} TO \'{3}\'@\'{4}\''.format( + grant, db, table, user, host + ) if grant_option: - query += " WITH GRANT OPTION" - log.debug("Query generated: {0}".format(query,)) + query += ' WITH GRANT OPTION' + log.debug('Query generated: {0}'.format(query)) return query @@ -644,17 +668,17 @@ def user_grants(user, salt '*' mysql.user_grants 'frank' 'localhost' ''' - if not user_exists(user,host): - log.info("User '{0}'@'{1}' does not exist".format(user,host,)) - return False + if not user_exists(user, host): + log.info('User \'{0}\'@\'{1}\' does not exist'.format(user, host)) + return False ret = [] db = connect() cur = db.cursor() - query = "SHOW GRANTS FOR '%s'@'%s'" % (user,host,) - log.debug("Doing query: {0}".format(query,)) + query = 'SHOW GRANTS FOR \'{0}\'@\'{1}\''.format(user, host) + log.debug('Doing query: {0}'.format(query)) - cur.execute( query ) + cur.execute(query) results = cur.fetchall() for grant in results: ret.append(grant[0].split(' IDENTIFIED BY')[0]) @@ -668,15 +692,19 @@ def grant_exists(grant, host='localhost', grant_option=False, escape=True): - # todo: This function is a bit tricky, since it requires the ordering to be exactly the same. - # perhaps should be replaced/reworked with a better/cleaner solution. - target = __grant_generate(grant, database, user, host, grant_option, escape) + # TODO: This function is a bit tricky, since it requires the ordering to + # be exactly the same. Perhaps should be replaced/reworked with a + # better/cleaner solution. + target = __grant_generate( + grant, database, user, host, grant_option, escape + ) - if target in user_grants(user, host): - log.debug("Grant exists.") + grants = user_grants(user, host) + if grants is not False and target in grants: + log.debug('Grant exists.') return True - log.debug("Grant does not exist, or is perhaps not ordered properly?") + log.debug('Grant does not exist, or is perhaps not ordered properly?') return False @@ -690,23 +718,31 @@ def grant_add(grant, Adds a grant to the MySQL server. For database, make sure you specify database.table or database.* - + CLI Example:: - salt '*' mysql.grant_add 'SELECT|INSERT|UPDATE|...' 'database.*' 'frank' 'localhost' + salt '*' mysql.grant_add 'SELECT,INSERT,UPDATE,...' 'database.*' 'frank' 'localhost' ''' # todo: validate grant db = connect() cur = db.cursor() query = __grant_generate(grant, database, user, host, grant_option, escape) - log.debug("Query: {0}".format(query,)) - cur.execute( query ) + log.debug('Query: {0}'.format(query)) + cur.execute(query) if grant_exists(grant, database, user, host, grant_option, escape): - log.info("Grant '{0}' on '{1}' for user '{2}' has been added".format(grant,database,user,)) + log.info( + 'Grant \'{0}\' on \'{1}\' for user \'{2}\' has been added'.format( + grant, database, user + ) + ) return True - log.info("Grant '{0}' on '{1}' for user '{2}' has NOT been added".format(grant,database,user,)) + log.info( + 'Grant \'{0}\' on \'{1}\' for user \'{2}\' has NOT been added'.format( + grant, database, user + ) + ) return False @@ -728,13 +764,21 @@ def grant_revoke(grant, cur = db.cursor() if grant_option: - grant += ", GRANT OPTION" - query = "REVOKE %s ON %s FROM '%s'@'%s';" % (grant, database, user, host,) - log.debug("Query: {0}".format(query,)) - cur.execute( query ) + grant += ', GRANT OPTION' + query = 'REVOKE {0} ON {1} FROM \'{2}\'@\'{3}\';'.format( + grant, database, user, host + ) + log.debug('Query: {0}'.format(query)) + cur.execute(query) if not grant_exists(grant, database, user, host, grant_option, escape): - log.info("Grant '{0}' on '{1}' for user '{2}' has been revoked".format(grant,database,user,)) + log.info( + 'Grant \'{0}\' on \'{1}\' for user \'{2}\' has been ' + 'revoked'.format(grant, database, user) + ) return True - log.info("Grant '{0}' on '{1}' for user '{2}' has NOT been revoked".format(grant,database,user,)) + log.info( + 'Grant \'{0}\' on \'{1}\' for user \'{2}\' has NOT been ' + 'revoked'.format(grant, database, user) + ) return False diff --git a/salt/modules/network.py b/salt/modules/network.py index abd4649553..60095fc6a7 100644 --- a/salt/modules/network.py +++ b/salt/modules/network.py @@ -1,9 +1,11 @@ ''' Module for gathering and managing network information ''' - +# Import Python libs import sys -from string import ascii_letters, digits +import logging + +# Import Salt libs from salt.utils.interfaces import * from salt.utils.socket_util import * @@ -13,11 +15,13 @@ __outputter__ = { 'netstat': 'txt', } +log = logging.getLogger(__name__) + + def __virtual__(): ''' Only work on posix-like systems ''' - # Disable on Windows, a specific file module exists: if __grains__['os'] in ('Windows',): return False @@ -25,14 +29,6 @@ def __virtual__(): return 'network' -def _sanitize_host(host): - ''' - Sanitize host string. - ''' - return "".join([ - c for c in host[0:255] if c in (ascii_letters + digits + '.-') - ]) - def _cidr_to_ipv4_netmask(cidr_bits): ''' Returns an IPv4 netmask @@ -45,7 +41,7 @@ def _cidr_to_ipv4_netmask(cidr_bits): netmask += '255' cidr_bits -= 8 else: - netmask += '%d' % (256-(2**(8-cidr_bits))) + netmask += '{0:d}'.format(256-(2**(8-cidr_bits))) cidr_bits = 0 return netmask @@ -58,6 +54,7 @@ def _number_of_set_bits_to_ipv4_netmask(set_bits): ''' return _cidr_to_ipv4_netmask(_number_of_set_bits(set_bits)) + def _number_of_set_bits(x): ''' Returns the number of bits that are set in a 32bit int @@ -80,10 +77,10 @@ def _interfaces_ip(out): ret = dict() def parse_network(value, cols): - """ + ''' Return a tuple of ip, netmask, broadcast based on the current set of cols - """ + ''' brd = None if '/' in value: # we have a CIDR in this address ip, cidr = value.split('/') @@ -109,7 +106,7 @@ def _interfaces_ip(out): continue m = re.match('^\d*:\s+([\w.]+)(?:@)?(\w+)?:\s+<(.+)>', line) if m: - iface,parent,attrs = m.groups() + iface, parent, attrs = m.groups() if 'UP' in attrs.split(','): data['up'] = True else: @@ -120,7 +117,7 @@ def _interfaces_ip(out): cols = line.split() if len(cols) >= 2: - type,value = tuple(cols[0:2]) + type, value = tuple(cols[0:2]) if type in ('inet', 'inet6'): if 'secondary' not in cols: ipaddr, netmask, broadcast = parse_network(value, cols) @@ -225,6 +222,13 @@ def _interfaces_ifconfig(out): def interfaces(): + ''' + Return a dictionary of information about all the interfaces on the minion + + CLI Example:: + + salt '*' network.interfaces + ''' ifaces = dict() if __salt__['cmd.has_exec']('ip'): cmd = __salt__['cmd.run']('ip addr show') @@ -235,6 +239,82 @@ def interfaces(): return ifaces +def _get_net_start(ipaddr, netmask): + ipaddr_octets = ipaddr.split('.') + netmask_octets = netmask.split('.') + net_start_octets = [str(int(ipaddr_octets[x]) & int(netmask_octets[x])) + for x in range(0, 4)] + return '.'.join(net_start_octets) + + +def _get_net_size(mask): + binary_str = '' + for octet in mask.split('.'): + binary_str += bin(int(octet))[2:].zfill(8) + return len(binary_str.rstrip('0')) + + +def _calculate_subnet(ipaddr, netmask): + return '{0}/{1}'.format(_get_net_start(ipaddr, netmask), + _get_net_size(netmask)) + + +def _ipv4_to_bits(ipaddr): + ''' + Accepts an IPv4 dotted quad and returns a string representing its binary + counterpart + ''' + return ''.join([bin(int(x))[2:].rjust(8, '0') for x in ipaddr.split('.')]) + + +def subnets(): + ''' + Returns a list of subnets to which the host belongs + ''' + ifaces = interfaces() + subnets = [] + + for ipv4_info in ifaces.values(): + for ipv4 in ipv4_info.get('inet', []): + if ipv4['address'] == '127.0.0.1': continue + network = _calculate_subnet(ipv4['address'], ipv4['netmask']) + subnets.append(network) + return subnets + + +def in_subnet(cidr): + ''' + Returns True if host is within specified subnet, otherwise False + ''' + try: + netstart, netsize = cidr.split('/') + netsize = int(netsize) + except: + log.error('Invalid CIDR \'{0}\''.format(cidr)) + return False + + ifaces = interfaces() + + netstart_bin = _ipv4_to_bits(netstart) + + if netsize < 32 and len(netstart_bin.rstrip('0')) > netsize: + log.error('Invalid network starting IP \'{0}\' in CIDR ' + '\'{1}\''.format(netstart, cidr)) + return False + + netstart_leftbits = netstart_bin[0:netsize] + for ipv4_info in ifaces.values(): + for ipv4 in ipv4_info.get('inet', []): + if ipv4['address'] == '127.0.0.1': continue + if netsize == 32: + if netstart == ipv4['address']: return True + else: + ip_leftbits = _ipv4_to_bits(ipv4['address'])[0:netsize] + if netstart_leftbits == ip_leftbits: return True + + return False + + def ping(host): ''' Performs a ping to a host @@ -243,7 +323,7 @@ def ping(host): salt '*' network.ping archlinux.org ''' - cmd = 'ping -c 4 %s' % _sanitize_host(host) + cmd = 'ping -c 4 {0}'.format(_sanitize_host(host)) return __salt__['cmd.run'](cmd) @@ -296,7 +376,7 @@ def traceroute(host): salt '*' network.traceroute archlinux.org ''' ret = [] - cmd = 'traceroute %s' % _sanitize_host(host) + cmd = 'traceroute {0}'.format(_sanitize_host(host)) out = __salt__['cmd.run'](cmd) for line in out: @@ -327,7 +407,5 @@ def dig(host): salt '*' network.dig archlinux.org ''' - cmd = 'dig %s' % _sanitize_host(host) + cmd = 'dig {0}'.format(_sanitize_host(host)) return __salt__['cmd.run'](cmd) - - diff --git a/salt/modules/nginx.py b/salt/modules/nginx.py index d0d2d05171..34c08b6a32 100644 --- a/salt/modules/nginx.py +++ b/salt/modules/nginx.py @@ -35,7 +35,7 @@ def version(): def signal(signal=None): ''' - Signals httpd to start, restart, or stop. + Signals nginx to start, restart, or stop. CLI Example:: diff --git a/salt/modules/nzbget.py b/salt/modules/nzbget.py index 98d5a512ee..7293348588 100644 --- a/salt/modules/nzbget.py +++ b/salt/modules/nzbget.py @@ -1,9 +1,10 @@ ''' Support for nzbget ''' - +# Import Salt libs import salt.utils + def __virtual__(): ''' Only load the module if apache is installed @@ -13,6 +14,7 @@ def __virtual__(): return 'nzbget' return False + def version(): ''' Return version from nzbget -v. @@ -24,7 +26,8 @@ def version(): cmd = 'nzbget -v' out = __salt__['cmd.run'](cmd).split('\n') ret = out[0].split(': ') - return { 'version': ret[1] } + return {'version': ret[1] } + def serverversion(): ''' @@ -42,8 +45,9 @@ def serverversion(): cmd = 'nzbget -V -c ~' + user + '/.nzbget | grep "server returned"' out = __salt__['cmd.run'](cmd).split('\n') ret = out[0].split(': ') - return { 'user': user, - 'version': ret[1], } + return {'user': user, + 'version': ret[1], } + def start(user=None): ''' @@ -58,7 +62,8 @@ def start(user=None): if user: cmd = 'su - ' + user + ' -c "' + cmd + '"' out = __salt__['cmd.run'](cmd).split('\n') - return cmd + return out + def stop(user=None): ''' @@ -73,7 +78,8 @@ def stop(user=None): if user: cmd = 'su - ' + user + ' -c "' + cmd + '"' out = __salt__['cmd.run'](cmd).split('\n') - return cmd + return out + def list(user=None): ''' @@ -111,6 +117,7 @@ def list(user=None): ret['Queue List'] = queuelist return ret + def pause(user=None): ''' Pause nzbget daemon using -P option. @@ -124,7 +131,8 @@ def pause(user=None): if user: cmd = cmd + ' -c ~' + user + '/.nzbget' out = __salt__['cmd.run'](cmd).split('\n') - return cmd + return out + def unpause(user=None): ''' @@ -139,5 +147,5 @@ def unpause(user=None): if user: cmd = cmd + ' -c ~' + user + '/.nzbget' out = __salt__['cmd.run'](cmd).split('\n') - return cmd + return out diff --git a/salt/modules/openbsdpkg.py b/salt/modules/openbsdpkg.py index 6abaf23ad1..37c748b3a2 100644 --- a/salt/modules/openbsdpkg.py +++ b/salt/modules/openbsdpkg.py @@ -72,7 +72,7 @@ def _format_pkgs(split): pkg = {} for k, v in split.items(): if v[2]: - name = '%s--%s' % (v[0], v[2]) + name = '{0}--{1}'.format(v[0], v[2]) else: name = v[0] pkg[name] = v[1] diff --git a/salt/modules/openbsdservice.py b/salt/modules/openbsdservice.py index 0159028d0f..122da8f1b9 100644 --- a/salt/modules/openbsdservice.py +++ b/salt/modules/openbsdservice.py @@ -1,9 +1,10 @@ ''' The service module for OpenBSD ''' - +# Import Python libs import os - +# Import Salt libs +import salt.utils # XXX enable/disable support would be nice @@ -53,11 +54,13 @@ def restart(name): salt '*' service.restart ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) cmd = '/etc/rc.d/{0} -f restart'.format(name) return not __salt__['cmd.retcode'](cmd) -def status(name): +def status(name, sig=None): ''' Return the status for a service, returns a bool whether the service is running. @@ -66,5 +69,7 @@ def status(name): salt '*' service.status ''' + if sig: + return bool(__salt__['status.pid'](sig)) cmd = '/etc/rc.d/{0} -f check'.format(name) return not __salt__['cmd.retcode'](cmd) diff --git a/salt/modules/pecl.py b/salt/modules/pecl.py new file mode 100644 index 0000000000..ff08fbd827 --- /dev/null +++ b/salt/modules/pecl.py @@ -0,0 +1,73 @@ +''' +Manage PHP pecl extensions. +''' + +# Import python libs +import re + + +__opts__ = {} +__pillar__ = {} + +def _pecl(command): + cmdline = 'pecl {0}'.format(command) + + ret = __salt__['cmd.run_all'](cmdline) + + if ret['retcode'] == 0: + return ret['stdout'] + else: + return False + + +def install(pecls): + ''' + Installs one or several pecl extensions. + + pecls + The pecl extensions to install. + ''' + return _pecl('install {0}'.format(pecls)) + + +def uninstall(pecls): + ''' + Uninstall one or several pecl extensions. + + pecls + The pecl extensions to uninstall. + ''' + return _pecl('uninstall {0}'.format(pecls)) + + +def update(pecls): + ''' + Update one or several pecl exntesions. + + pecls + The pecl extensions to update. + ''' + return _pecl('install -U {0}'.format(pecls)) + + +def list(): + ''' + List installed pecl extensions. + ''' + + lines = _pecl('list').splitlines() + lines.pop(0) + lines.pop(0) + lines.pop(0) + lines.pop(0) + + pecls = {} + for line in lines: + m = re.match('^([^ ]+)[ ]+([^ ]+)[ ]+([^ ]+)', line) + if m: + pecls[m.group(1)] = [m.group(2), m.group(3)] + + return pecls + + + diff --git a/salt/modules/pip.py b/salt/modules/pip.py index 3801ed7c70..2f453c0e71 100644 --- a/salt/modules/pip.py +++ b/salt/modules/pip.py @@ -1,9 +1,19 @@ ''' Install Python packages with pip to either the system or a virtualenv ''' - -# Import python libs +# Import Python libs import os +import logging +import tempfile +import shutil +# Import Salt libs +from salt.exceptions import CommandExecutionError, CommandNotFoundError + +# It would be cool if we could use __virtual__() in this module, though, since +# pip can be installed on a virtualenv anywhere on the filesystem, there's no +# definite way to tell if pip is installed on not. + +logger = logging.getLogger(__name__) def _get_pip_bin(bin_env): ''' @@ -11,17 +21,19 @@ def _get_pip_bin(bin_env): passed in, or from the global modules options ''' if not bin_env: - pips = ['pip2', - 'pip', - 'pip-python'] - return __salt__['cmd.which_bin'](pips) - else: - # try to get pip bin from env - if os.path.exists(os.path.join(bin_env, 'bin', 'pip')): - pip_bin = os.path.join(bin_env, 'bin', 'pip') - else: - pip_bin = bin_env - return pip_bin + which_result = __salt__['cmd.which_bin'](['pip2', 'pip', 'pip-python']) + if which_result is None: + raise CommandNotFoundError('Could not find a `pip` binary') + return which_result + + # try to get pip bin from env + if os.path.isdir(bin_env): + pip_bin = os.path.join(bin_env, 'bin', 'pip') + if os.path.isfile(pip_bin): + return pip_bin + raise CommandNotFoundError('Could not find a `pip` binary') + + return bin_env def install(pkgs=None, @@ -68,7 +80,7 @@ def install(pkgs=None, If installing into a virtualenv, just use the path to the virtualenv (/home/code/path/to/virtualenv/) env - depreicated, use bin_env now + deprecated, use bin_env now log Log file where a complete (maximum verbosity) record will be kept proxy @@ -160,17 +172,32 @@ def install(pkgs=None, cmd = '{cmd} {pkg} '.format( cmd=cmd, pkg=pkg) + treq = None if requirements: + if requirements.startswith('salt://'): + req = __salt__['cp.cache_file'](requirements) + fd_, treq = tempfile.mkstemp() + os.close(fd_) + shutil.copyfile(req, treq) + else: + treq = requirements cmd = '{cmd} --requirement "{requirements}" '.format( - cmd=cmd, requirements=requirements) + cmd=cmd, requirements=treq or requirements) + + if treq is not None and runas: + logger.debug( + 'Changing ownership of requirements file \'{0}\' to ' + 'user \'{1}\''.format(treq, runas) + ) + __salt__['file.chown'](treq, runas, None) if log: try: # TODO make this check if writeable os.path.exists(log) except IOError: - raise IOError("'%s' is not writeable" % log) - cmd = '{cmd} --{log} '.format( + raise IOError('\'{0}\' is not writeable'.format(log)) + cmd = '{cmd} --log {log} '.format( cmd=cmd, log=log) if proxy: @@ -181,7 +208,9 @@ def install(pkgs=None, try: int(timeout) except ValueError: - raise ValueError("'%s' is not a valid integer base 10.") + raise ValueError( + '\'{0}\' is not a valid integer base 10.'.format(timeout) + ) cmd = '{cmd} --timeout={timeout} '.format( cmd=cmd, timeout=timeout) @@ -193,19 +222,21 @@ def install(pkgs=None, if find_links: if not find_links.startswith("http://"): - raise Exception("'%s' must be a valid url" % find_links) + raise Exception('\'{0}\' must be a valid url'.format(find_links)) cmd = '{cmd} --find-links={find_links}'.format( cmd=cmd, find_links=find_links) if index_url: if not index_url.startswith("http://"): - raise Exception("'%s' must be a valid url" % index_url) + raise Exception('\'{0}\' must be a valid url'.format(index_url)) cmd = '{cmd} --index-url="{index_url}" '.format( cmd=cmd, index_url=index_url) if extra_index_url: if not extra_index_url.startswith("http://"): - raise Exception("'%s' must be a valid url" % extra_index_url) + raise Exception( + '\'{0}\' must be a valid url'.format(extra_index_url) + ) cmd = '{cmd} --extra-index_url="{extra_index_url}" '.format( cmd=cmd, extra_index_url=extra_index_url) @@ -214,7 +245,7 @@ def install(pkgs=None, if mirrors: if not mirrors.startswith("http://"): - raise Exception("'%s' must be a valid url" % mirrors) + raise Exception('\'{0}\' must be a valid url'.format(mirrors)) cmd = '{cmd} --use-mirrors --mirrors={mirrors} '.format( cmd=cmd, mirrors=mirrors) @@ -260,7 +291,16 @@ def install(pkgs=None, cmd = '{cmd} --install-options={install_options} '.format( cmd=cmd, install_options=install_options) - return __salt__['cmd.run'](cmd, runas=runas, cwd=cwd) + try: + result = __salt__['cmd.run_all'](cmd, runas=runas, cwd=cwd) + finally: + if treq: + try: + os.remove(treq) + except Exception: + pass + + return result def uninstall(pkgs=None, @@ -321,16 +361,22 @@ def uninstall(pkgs=None, cmd = '{cmd} {pkg} '.format( cmd=cmd, pkg=pkg) + treq = None if requirements: + if requirements.startswith('salt://'): + req = __salt__['cp.cache_file'](requirements) + fd_, treq = tempfile.mkstemp() + os.close(fd_) + shutil.copyfile(req, treq) cmd = '{cmd} --requirements "{requirements}" '.format( - cmd=cmd, requirements=requirements) + cmd=cmd, requirements=treq or requirements) if log: try: # TODO make this check if writeable os.path.exists(log) except IOError: - raise IOError("'%s' is not writeable" % log) + raise IOError('\'{0}\' is not writeable'.format(log)) cmd = '{cmd} --{log} '.format( cmd=cmd, log=log) @@ -342,11 +388,21 @@ def uninstall(pkgs=None, try: int(timeout) except ValueError: - raise ValueError("'%s' is not a valid integer base 10.") + raise ValueError( + '\'{0}\' is not a valid integer base 10.'.format(timeout) + ) cmd = '{cmd} --timeout={timeout} '.format( cmd=cmd, timeout=timeout) - return __salt__['cmd.run'](cmd, runas=runas, cwd=cwd).split('\n') + result = __salt__['cmd.run_all'](cmd, runas=runas, cwd=cwd) + + if treq: + try: + os.remove(treq) + except Exception: + pass + + return result def freeze(bin_env=None, @@ -372,9 +428,22 @@ def freeze(bin_env=None, salt '*' pip.freeze /home/code/path/to/virtualenv/ ''' - cmd = '{0} freeze'.format(_get_pip_bin(bin_env)) + pip_bin = _get_pip_bin(bin_env) - return __salt__['cmd.run'](cmd, runas=runas, cwd=cwd).split('\n') + activate = os.path.join(os.path.dirname(pip_bin), 'activate') + if not os.path.isfile(activate): + raise CommandExecutionError( + "Could not find the path to the virtualenv's 'activate' binary" + ) + + cmd = 'source {0}; {1} freeze'.format(activate, pip_bin) + + result = __salt__['cmd.run_all'](cmd, runas=runas, cwd=cwd) + + if result['retcode'] > 0: + raise CommandExecutionError(result['stderr']) + + return result['stdout'].split('\n') def list(prefix='', @@ -382,27 +451,33 @@ def list(prefix='', runas=None, cwd=None): ''' - Filter list of instaslled apps from ``freeze`` and check to see if ``prefix`` - exists in the list of packages installed. + Filter list of installed apps from ``freeze`` and check to see if + ``prefix`` exists in the list of packages installed. CLI Example:: salt '*' pip.list salt ''' packages = {} + cmd = '{0} freeze'.format(_get_pip_bin(bin_env)) - for line in __salt__['cmd.run'](cmd, runas=runas, cwd=cwd).split("\n"): + + result = __salt__['cmd.run_all'](cmd, runas=runas, cwd=cwd) + if result['retcode'] > 0: + raise CommandExecutionError(result['stderr']) + + for line in result['stdout'].split('\n'): if line.startswith('-e'): line = line.split('-e ')[1] line, name = line.split('#egg=') - packages[name]=line + packages[name] = line elif len(line.split("==")) >= 2: name = line.split("==")[0] version = line.split("==")[1] if prefix: if line.lower().startswith(prefix.lower()): - packages[name]=version + packages[name] = version else: - packages[name]=version + packages[name] = version return packages diff --git a/salt/modules/postgres.py b/salt/modules/postgres.py index 6caebb412f..2439dfcac2 100644 --- a/salt/modules/postgres.py +++ b/salt/modules/postgres.py @@ -46,7 +46,7 @@ def version(): version_line = __salt__['cmd.run']('psql --version').split("\n")[0] name = version_line.split(" ")[1] ver = version_line.split(" ")[2] - return "%s %s" % (name, ver) + return '{0} {1}'.format(name, ver) def _connection_defaults(user=None, host=None, port=None): ''' @@ -240,11 +240,16 @@ def user_list(user=None, host=None, port=None, runas=None): (user, host, port) = _connection_defaults(user, host, port) ret = [] - cmd = _psql_cmd('-c', 'SELECT * FROM pg_roles', + query = ( + '''SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, + rolcatupdate, rolcanlogin, rolconnlimit, rolvaliduntil, rolconfig, oid + FROM pg_roles''' + ) + cmd = _psql_cmd('-c', query, host=host, user=user, port=port) cmdret = __salt__['cmd.run'](cmd, runas=runas) - lines = [x for x in cmdret.splitlines() if len(x.split("|")) == 13] + lines = [x for x in cmdret.splitlines() if len(x.split("|")) == 11] log.debug(lines) header = [x.strip() for x in lines[0].split("|")] for line in lines[1:]: @@ -264,12 +269,18 @@ def user_exists(name, user=None, host=None, port=None, runas=None): ''' (user, host, port) = _connection_defaults(user, host, port) - users = user_list(user=user, host=host, port=port, runas=runas) - for user in users: - if name == dict(user).get('rolname'): - return True + query = ( + "SELECT true " + "FROM pg_roles " + "WHERE EXISTS " + "(SELECT rolname WHERE rolname='{role}')".format(role=name) + ) + cmd = _psql_cmd('-c', query, host=host, user=user, port=port) + cmdret = __salt__['cmd.run'](cmd, runas=runas) + log.debug(cmdret.splitlines()) + val = cmdret.splitlines()[1] + return True if val.strip() == 't' else False - return False def user_create(username, user=None, diff --git a/salt/modules/poudriere.py b/salt/modules/poudriere.py index 95523e088c..a5b7f847df 100644 --- a/salt/modules/poudriere.py +++ b/salt/modules/poudriere.py @@ -94,7 +94,7 @@ def make_pkgng_aware(jname): if os.path.isfile(os.path.join(cdir,jname) + '-make.conf'): ret['changes'] = 'Created {0}'.format( - os.path.join(cdir, '{0}-make.conf'.format(janme)) + os.path.join(cdir, '{0}-make.conf'.format(jname)) ) return ret else: diff --git a/salt/modules/publish.py b/salt/modules/publish.py index 5ad5fc6c62..ab3f0b525c 100644 --- a/salt/modules/publish.py +++ b/salt/modules/publish.py @@ -5,14 +5,10 @@ Publish a command from a minion to a target # Import python libs import ast -# Import third party libs -import zmq - # Import salt libs import salt.crypt import salt.payload -from salt._compat import string_types -from salt.exceptions import SaltReqTimeoutError +from salt._compat import string_types, integer_types def _publish( tgt, @@ -39,20 +35,10 @@ def _publish( salt system.example.com publish.publish '*' cmd.run 'ls -la /tmp' ''' - serial = salt.payload.Serial(__opts__) if fun == 'publish.publish': # Need to log something here return {} - - if not arg: - arg = [] - - try: - if isinstance(ast.literal_eval(arg), dict): - arg = [arg,] - except Exception: - if isinstance(arg, string_types): - arg = arg.split(',') + arg = normalize_arg(arg) sreq = salt.payload.SREQ(__opts__['master_uri']) auth = salt.crypt.SAuth(__opts__) @@ -71,6 +57,19 @@ def _publish( return auth.crypticle.loads( sreq.send('aes', auth.crypticle.dumps(load), 1)) +def normalize_arg(arg): + if not arg: + arg = [] + + try: + # Numeric checks here because of all numeric strings, like JIDs + if isinstance(ast.literal_eval(arg), (dict,integer_types,long)): + arg = [arg,] + except Exception: + if isinstance(arg, string_types): + arg = arg.split(',') + + return arg def publish(tgt, fun, arg=None, expr_form='glob', returner='', timeout=5): ''' @@ -114,18 +113,9 @@ def runner(fun, arg=None): salt publish.runner manage.down ''' - serial = salt.payload.Serial(__opts__) - if not arg: - arg = [] + arg = normalize_arg(arg) - try: - if isinstance(ast.literal_eval(arg), dict): - arg = [arg,] - except Exception: - if isinstance(arg, string_types): - arg = arg.split(',') - - sreq = salt.payload(__opts__['master_uri']) + sreq = salt.payload.SREQ(__opts__['master_uri']) auth = salt.crypt.SAuth(__opts__) tok = auth.gen_token('salt') load = { diff --git a/salt/modules/pw_user.py b/salt/modules/pw_user.py index e048d83093..2e860c4b41 100644 --- a/salt/modules/pw_user.py +++ b/salt/modules/pw_user.py @@ -62,7 +62,7 @@ def delete(name, remove=False, force=False): CLI Example:: - salt '*' user.delete name True True + salt '*' user.delete name remove=True force=True ''' cmd = 'pw userdel ' if remove: @@ -219,6 +219,9 @@ def list_groups(name): salt '*' user.list_groups foo ''' ugrp = set() + # Add the primary user's group + ugrp.add(grp.getgrgid(pwd.getpwnam(name).pw_gid).gr_name) + # Now, all other groups the user belongs to for group in grp.getgrall(): if name in group.gr_mem: ugrp.add(group.gr_name) diff --git a/salt/modules/reg.py b/salt/modules/reg.py index 3f30304de9..4a2aa1ed37 100644 --- a/salt/modules/reg.py +++ b/salt/modules/reg.py @@ -17,8 +17,10 @@ except ImportError: except ImportError: has_windows_modules = False -import salt.utils +# Import Python libs import logging +# Import Salt libs +import salt.utils from salt.exceptions import CommandExecutionError log = logging.getLogger(__name__) @@ -29,7 +31,7 @@ class Registry(object): Delay '_winreg' usage until this module is used ''' def __init__(self): - hkeys = { + self.hkeys = { "HKEY_USERS": _winreg.HKEY_USERS, "HKEY_CURRENT_USER": _winreg.HKEY_CURRENT_USER, "HKEY_LOCAL_MACHINE": _winreg.HKEY_LOCAL_MACHINE, diff --git a/salt/modules/rh_ip.py b/salt/modules/rh_ip.py index b7ff293d26..6aaa6b414a 100644 --- a/salt/modules/rh_ip.py +++ b/salt/modules/rh_ip.py @@ -5,6 +5,7 @@ The networking module for RHEL/Fedora based distros import logging import re from os.path import exists, join +import StringIO # import third party libs import jinja2 @@ -20,8 +21,7 @@ def __virtual__(): ''' Confine this module to RHEL/Fedora based distros$ ''' - dists = ('CentOS', 'Scientific', 'RedHat', 'Fedora') - if __grains__['os'] in dists: + if __grains__['os_family'] == 'RedHat': return 'ip' return False @@ -52,7 +52,7 @@ _CONFIG_TRUE = ['yes', 'on', 'true', '1', True] _CONFIG_FALSE = ['no', 'off', 'false', '0', False] _IFACE_TYPES = [ 'eth', 'bond', 'alias', 'clone', - 'ipsec', 'dialup', 'slave', 'vlan', + 'ipsec', 'dialup', 'bridge', 'slave', 'vlan', ] @@ -230,7 +230,6 @@ def _parse_settings_bond_0(opts, iface, bond_def): valid = ['list of ips (up to 16)'] if 'arp_ip_target' in opts: if isinstance(opts['arp_ip_target'], list): - target_length = len(opts['arp_ip_target']) if 1 <= len(opts['arp_ip_target']) <= 16: bond.update({'arp_ip_target': []}) for ip in opts['arp_ip_target']: @@ -513,10 +512,11 @@ def _parse_settings_eth(opts, iface_type, enabled, iface): if 'dns' in opts: result['dns'] = opts['dns'] result['peernds'] = 'yes' - - ethtool = _parse_ethtool_opts(opts, iface) - if ethtool: - result['ethtool'] = ethtool + + if iface_type not in ['bridge']: + ethtool = _parse_ethtool_opts(opts, iface) + if ethtool: + result['ethtool'] = ethtool if iface_type == 'slave': result['proto'] = 'none' @@ -526,7 +526,7 @@ def _parse_settings_eth(opts, iface_type, enabled, iface): if bonding: result['bonding'] = bonding - if iface_type not in ['bond', 'vlan']: + if iface_type not in ['bond', 'vlan', 'bridge']: if 'addr' in opts: if _MAC_REGEX.match(opts['addr']): result['addr'] = opts['addr'] @@ -537,7 +537,31 @@ def _parse_settings_eth(opts, iface_type, enabled, iface): if iface in ifaces and 'hwaddr' in ifaces[iface]: result['addr'] = ifaces[iface]['hwaddr'] - for opt in ['ipaddr', 'master', 'netmask', 'srcaddr']: + if iface_type == 'bridge': + result['devtype'] = 'Bridge' + bypassfirewall = True + valid = _CONFIG_TRUE + _CONFIG_FALSE + for opt in ['bypassfirewall']: + if opt in opts: + if opts[opt] in _CONFIG_TRUE: + bypassfirewall = True + elif opts[opt] in _CONFIG_FALSE: + bypassfirewall = False + else: + _raise_error_iface(iface, opts[opt], valid) + if bypassfirewall: + __salt__['sysctl.persist']('net.bridge.bridge-nf-call-ip6tables', '0') + __salt__['sysctl.persist']('net.bridge.bridge-nf-call-iptables', '0') + __salt__['sysctl.persist']('net.bridge.bridge-nf-call-arptables', '0') + else: + __salt__['sysctl.persist']('net.bridge.bridge-nf-call-ip6tables', '1') + __salt__['sysctl.persist']('net.bridge.bridge-nf-call-iptables', '1') + __salt__['sysctl.persist']('net.bridge.bridge-nf-call-arptables', '1') + else: + if 'bridge' in opts: + result['bridge'] = opts['bridge'] + + for opt in ['ipaddr', 'master', 'netmask', 'srcaddr', 'delay']: if opt in opts: result[opt] = opts[opt] @@ -612,13 +636,14 @@ def _parse_network_settings(opts, current): else: _raise_error_network('hostname', ['server1.example.com']) - if opts['nozeroconf'] in valid: - if opts['nozeroconf'] in _CONFIG_TRUE: - result['nozeroconf'] = 'true' - elif opts['nozeroconf'] in _CONFIG_FALSE: - result['nozeroconf'] = 'false' - else: - _raise_error_network('nozeroconf', valid) + if 'nozeroconf' in opts: + if opts['nozeroconf'] in valid: + if opts['nozeroconf'] in _CONFIG_TRUE: + result['nozeroconf'] = 'true' + elif opts['nozeroconf'] in _CONFIG_FALSE: + result['nozeroconf'] = 'false' + else: + _raise_error_network('nozeroconf', valid) for opt in opts: if opt not in ['networking', 'hostname', 'nozeroconf']: @@ -677,6 +702,14 @@ def _write_file_network(data, filename): fout.write(data) fout.close() +def _read_temp(data): + tout = StringIO.StringIO() + tout.write(data) + tout.seek(0) + output = tout.readlines() + tout.close() + return output + def build_bond(iface, settings): ''' @@ -688,7 +721,6 @@ def build_bond(iface, settings): salt '*' ip.build_bond bond0 mode=balance-alb ''' rh_major = __grains__['osrelease'][:1] - rh_minor = __grains__['osrelease'][2:] opts = _parse_settings_bond(settings, iface) template = env.get_template('conf.jinja') @@ -701,6 +733,9 @@ def build_bond(iface, settings): __salt__['cmd.run']('cat {0} >> /etc/modprobe.conf'.format(path)) __salt__['kmod.load']('bonding') + if settings['test']: + return _read_temp(data) + return _read_file(path) @@ -713,7 +748,9 @@ def build_interface(iface, iface_type, enabled, settings): salt '*' ip.build_interface eth0 eth ''' rh_major = __grains__['osrelease'][:1] - rh_minor = __grains__['osrelease'][2:] + + iface = iface.lower() + iface_type = iface_type.lower() if iface_type not in _IFACE_TYPES: _raise_error_iface(iface, iface_type, _IFACE_TYPES) @@ -728,13 +765,20 @@ def build_interface(iface, iface_type, enabled, settings): if iface_type == 'vlan': settings['vlan'] = 'yes' - if iface_type in ['eth', 'bond', 'slave', 'vlan']: + if iface_type == 'bridge': + __salt__['pkg.install']('bridge-utils') + + if iface_type in ['eth', 'bond', 'bridge', 'slave', 'vlan']: opts = _parse_settings_eth(settings, iface_type, enabled, iface) template = env.get_template('rh{0}_eth.jinja'.format(rh_major)) ifcfg = template.render(opts) + if settings['test']: + return _read_temp(ifcfg) + _write_file_iface(iface, ifcfg, _RH_NETWORK_SCRIPT_DIR, 'ifcfg-{0}') path = join(_RH_NETWORK_SCRIPT_DIR, 'ifcfg-{0}'.format(iface)) + return _read_file(path) @@ -833,6 +877,9 @@ def build_network_settings(settings): opts = _parse_network_settings(settings,current_network_settings) template = env.get_template('network.jinja') network = template.render(opts) + + if settings['test']: + return _read_temp(network) # Wirte settings _write_file_network(network, _RH_NETWORK_FILE) diff --git a/salt/modules/rh_ip/network.jinja b/salt/modules/rh_ip/network.jinja index ec2565992d..4006b12d10 100644 --- a/salt/modules/rh_ip/network.jinja +++ b/salt/modules/rh_ip/network.jinja @@ -1,6 +1,6 @@ {% if networking %}NETWORKING={{networking}} {%endif%}{% if hostname %}HOSTNAME={{hostname}} {%endif%}{% if gateway %}GATEWAY={{gateway}} -{%endif%}{% if gatewaydev %}GATEWAYDEV={{gatewaydev}}{%endif%} -{%endif%}{% if nisdomain %}NISDOMAIN={{nisdomain}}{%endif%} +{%endif%}{% if gatewaydev %}GATEWAYDEV={{gatewaydev}} +{%endif%}{% if nisdomain %}NISDOMAIN={{nisdomain}} {%endif%}{% if nozeroconf %}NOZEROCONF={{nozeroconf}}{%endif%} diff --git a/salt/modules/rh_ip/rh5_eth.jinja b/salt/modules/rh_ip/rh5_eth.jinja index 91fe0ed98e..e7236415d5 100644 --- a/salt/modules/rh_ip/rh5_eth.jinja +++ b/salt/modules/rh_ip/rh5_eth.jinja @@ -4,6 +4,7 @@ DEVICE={{name}} {%endif%}{% if master %}MASTER={{master}} {%endif%}{% if slave %}SLAVE={{slave}} {%endif%}{% if vlan %}VLAN={{vlan}} +{%endif%}{% if devtype %}TYPE={{devtype}} {%endif%}{% if proto %}BOOTPROTO={{proto}} {%endif%}{% if onboot %}ONBOOT={{onboot}} {%endif%}{% if ipaddr %}IPADDR={{ipaddr}} @@ -11,6 +12,8 @@ DEVICE={{name}} {%endif%}{% if gateway %}GATEWAY={{gateway}} {%endif%}{% if srcaddr %}SRCADDR={{srcaddr}} {%endif%}{% if peerdns %}PEERDNS={{peerdns}} +{%endif%}{% if bridge %}BRIDGE={{bridge}} +{%endif%}{% if delay %}DELAY={{delay}} {%endif%}{%if bonding %}BONDING_OPTS="{%for item in bonding %}{{item}}={{bonding[item]}} {%endfor%}" {%endif%}{% if ethtool %}ETHTOOL_OPTS="{%for item in ethtool %}{{item}} {{ethtool[item]}} {%endfor%}" {%endif%} diff --git a/salt/modules/rh_ip/rh6_eth.jinja b/salt/modules/rh_ip/rh6_eth.jinja index df48c5918f..d1bde1b18b 100644 --- a/salt/modules/rh_ip/rh6_eth.jinja +++ b/salt/modules/rh_ip/rh6_eth.jinja @@ -4,6 +4,7 @@ DEVICE="{{name}}" {%endif%}{% if master %}MASTER="{{master}}" {%endif%}{% if slave %}SLAVE="{{slave}}" {%endif%}{% if vlan %}VLAN="{{vlan}}" +{%endif%}{% if devtype %}TYPE="{{devtype}}" {%endif%}{% if proto %}BOOTPROTO="{{proto}}" {%endif%}{% if onboot %}ONBOOT="{{onboot}}" {%endif%}{% if ipaddr %}IPADDR="{{ipaddr}}" @@ -11,6 +12,8 @@ DEVICE="{{name}}" {%endif%}{% if gateway %}GATEWAY="{{gateway}}" {%endif%}{% if srcaddr %}SRCADDR="{{srcaddr}}" {%endif%}{% if peerdns %}PEERDNS="{{peerdns}}" +{%endif%}{% if bridge %}BRIDGE="{{bridge}}" +{%endif%}{% if delay %}DELAY="{{delay}}" {%endif%}{%if bonding %}BONDING_OPTS="{%for item in bonding %}{{item}}={{bonding[item]}} {%endfor%}" {%endif%}{% if ethtool %}ETHTOOL_OPTS="{%for item in ethtool %}{{item}} {{ethtool[item]}} {%endfor%}" {%endif%} diff --git a/salt/modules/rh_service.py b/salt/modules/rh_service.py index 2c98b552ed..503ef73610 100644 --- a/salt/modules/rh_service.py +++ b/salt/modules/rh_service.py @@ -3,6 +3,8 @@ Service support for classic Red Hat type systems. This interface uses the service command (so it is compatible with upstart systems) and the chkconfig command. ''' +# Import Salt libs +import salt.utils def __virtual__(): @@ -14,6 +16,8 @@ def __virtual__(): 'RedHat', 'CentOS', 'Scientific', + 'CloudLinux', + 'Amazon', 'Fedora', ] if __grains__['os'] in enable: @@ -120,6 +124,8 @@ def restart(name): salt '*' service.restart ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) cmd = '/sbin/service {0} restart'.format(name) return not __salt__['cmd.retcode'](cmd) @@ -133,6 +139,8 @@ def status(name, sig=None): salt '*' service.status ''' + if sig: + return bool(__salt__['status.pid'](sig)) cmd = '/sbin/service {0} status'.format(name) return not __salt__['cmd.retcode'](cmd) diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py index 4c043cccd5..9cd4892997 100644 --- a/salt/modules/saltutil.py +++ b/salt/modules/saltutil.py @@ -9,17 +9,34 @@ import hashlib import shutil import signal import logging +import sys # Import Salt libs import salt.payload +import salt.state from salt._compat import string_types +# Import esky for update functionality +try: + import esky + has_esky = True +except ImportError: + has_esky = False + log = logging.getLogger(__name__) -def _sync(form, env): +def _sync(form, env=None): ''' Sync the given directory in the given environment ''' + if env is None: + # No environment passed, detect them based on gathering the top files + # from the master + env = 'base' + st_ = salt.state.HighState(__opts__) + top = st_.get_top() + if top: + env = st_.top_matches(top).keys() if isinstance(env, string_types): env = env.split(',') ret = [] @@ -27,40 +44,120 @@ def _sync(form, env): source = os.path.join('salt://_{0}'.format(form)) mod_dir = os.path.join(__opts__['extension_modules'], '{0}'.format(form)) if not os.path.isdir(mod_dir): + log.info('Creating module dir \'{0}\''.format(mod_dir)) os.makedirs(mod_dir) - cache = [] for sub_env in env: + log.info('Syncing {0} for environment \'{1}\''.format(form, sub_env)) + cache = [] + log.info('Loading cache from {0}, for {1})'.format(source, sub_env)) cache.extend(__salt__['cp.cache_dir'](source, sub_env)) - for fn_ in cache: - remote.add(os.path.basename(fn_)) - dest = os.path.join(mod_dir, - os.path.basename(fn_) + local_cache_dir=os.path.join( + __opts__['cachedir'], + 'files', + sub_env, + '_{0}'.format(form) ) - if os.path.isfile(dest): - # The file is present, if the sum differes replace it - srch = hashlib.md5(open(fn_, 'r').read()).hexdigest() - dsth = hashlib.md5(open(dest, 'r').read()).hexdigest() - if srch != dsth: - # The downloaded file differes, replace! + log.debug('Local cache dir: \'{0}\''.format(local_cache_dir)) + for fn_ in cache: + relpath = os.path.relpath(fn_, local_cache_dir) + relname = os.path.splitext(relpath)[0].replace(os.sep, '.') + remote.add(relpath) + dest = os.path.join(mod_dir, relpath) + log.info('Copying \'{0}\' to \'{1}\''.format(fn_, dest)) + if os.path.isfile(dest): + # The file is present, if the sum differs replace it + srch = hashlib.md5(open(fn_, 'r').read()).hexdigest() + dsth = hashlib.md5(open(dest, 'r').read()).hexdigest() + if srch != dsth: + # The downloaded file differes, replace! + shutil.copyfile(fn_, dest) + ret.append('{0}.{1}'.format(form, relname)) + else: + dest_dir = os.path.dirname(dest) + if not os.path.isdir(dest_dir): + os.makedirs(dest_dir) shutil.copyfile(fn_, dest) - ret.append('{0}.{1}'.format(form, os.path.basename(fn_))) - else: - shutil.copyfile(fn_, dest) - ret.append('{0}.{1}'.format(form, os.path.basename(fn_))) - if ret: - mod_file = os.path.join(__opts__['cachedir'], 'module_refresh') - with open(mod_file, 'a+') as f: - f.write('') + ret.append('{0}.{1}'.format(form, relname)) + + touched = bool(ret) if __opts__.get('clean_dynamic_modules', True): - current = set(os.listdir(mod_dir)) + current = set(_listdir_recursively(mod_dir)) for fn_ in current - remote: full = os.path.join(mod_dir, fn_) if os.path.isfile(full): + touched = True os.remove(full) + #cleanup empty dirs + while True: + emptydirs = _list_emptydirs(mod_dir) + if not emptydirs: + break + for emptydir in emptydirs: + touched = True + os.rmdir(emptydir) + #dest mod_dir is touched? trigger reload if requested + if touched: + mod_file = os.path.join(__opts__['cachedir'], 'module_refresh') + with open(mod_file, 'a+') as f: + f.write('') return ret +def _listdir_recursively(rootdir): + fileList = [] + for root, subFolders, files in os.walk(rootdir): + for file in files: + relpath=os.path.relpath(root,rootdir).strip('.') + fileList.append(os.path.join(relpath,file)) + return fileList -def sync_modules(env='base'): +def _list_emptydirs(rootdir): + emptydirs = [] + for root, subFolders, files in os.walk(rootdir): + if not files and not subFolders: + emptydirs.append(root) + return emptydirs + +def update(version=None): + ''' + Update the salt minion from the url defined in opts['update_url'] + + + This feature requires the minion to be running a bdist_esky build. + + The version number is optional and will default to the most recent version + available at opts['update_url']. + + Returns details about the transaction upon completion. + + CLI Example:: + + salt '*' saltutil.update 0.10.3 + ''' + if not has_esky: + return "Esky not available as import" + if not getattr(sys, "frozen", False): + return "Minion is not running an Esky build" + if not __opts__['update_url']: + return "'update_url' not configured on this minion" + app = esky.Esky(sys.executable, __opts__['update_url']) + oldversion = __grains__['saltversion'] + try: + if not version: + version = app.find_update() + if not version: + return "No updates available" + app.fetch_version(version) + app.install_version(version) + app.cleanup() + except Exception as e: + return e + restarted = {} + for service in __opts__['update_restart_services']: + restarted[service] = __salt__['service.restart'](service) + return {'comment': 'Updated from {0} to {1}'.format(oldversion, version), + 'restarted': restarted} + +def sync_modules(env=None): ''' Sync the modules from the _modules directory on the salt master file server. This function is environment aware, pass the desired environment @@ -74,7 +171,7 @@ def sync_modules(env='base'): return _sync('modules', env) -def sync_states(env='base'): +def sync_states(env=None): ''' Sync the states from the _states directory on the salt master file server. This function is environment aware, pass the desired environment @@ -88,7 +185,7 @@ def sync_states(env='base'): return _sync('states', env) -def sync_grains(env='base'): +def sync_grains(env=None): ''' Sync the grains from the _grains directory on the salt master file server. This function is environment aware, pass the desired environment @@ -102,7 +199,7 @@ def sync_grains(env='base'): return _sync('grains', env) -def sync_renderers(env='base'): +def sync_renderers(env=None): ''' Sync the renderers from the _renderers directory on the salt master file server. This function is environment aware, pass the desired environment @@ -116,7 +213,7 @@ def sync_renderers(env='base'): return _sync('renderers', env) -def sync_returners(env='base'): +def sync_returners(env=None): ''' Sync the returners from the _returners directory on the salt master file server. This function is environment aware, pass the desired environment @@ -130,7 +227,7 @@ def sync_returners(env='base'): return _sync('returners', env) -def sync_all(env='base'): +def sync_all(env=None): ''' Sync down all of the dynamic modules from the file server for a specific environment @@ -139,6 +236,7 @@ def sync_all(env='base'): salt '*' saltutil.sync_all ''' + logging.debug("Syncing all") ret = [] ret.append(sync_modules(env)) ret.append(sync_states(env)) @@ -250,7 +348,7 @@ def term_job(jid): def kill_job(jid): ''' - Sends a termination signal (SIGTERM 15) to the named salt job's process + Sends a kill signal (SIGKILL 9) to the named salt job's process CLI Example:: diff --git a/salt/modules/service.py b/salt/modules/service.py index 2e68cde3b5..96101beeea 100644 --- a/salt/modules/service.py +++ b/salt/modules/service.py @@ -2,8 +2,11 @@ The default service module, if not otherwise specified salt will fall back to this basic module ''' - +# Import Python libs import os +# Import Salt libs +import salt.utils + grainmap = { 'Arch': '/etc/rc.d', @@ -13,6 +16,7 @@ grainmap = { 'Ubuntu': '/etc/init.d', 'Gentoo': '/etc/init.d', 'CentOS': '/etc/init.d', + 'CloudLinux': '/etc/init.d', 'Amazon': '/etc/init.d', 'SunOS': '/etc/init.d', } @@ -25,7 +29,9 @@ def __virtual__(): disable = [ 'RedHat', 'CentOS', + 'Amazon', 'Scientific', + 'CloudLinux', 'Fedora', 'Gentoo', 'Ubuntu', @@ -73,6 +79,8 @@ def restart(name): salt '*' service.restart ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) cmd = os.path.join(grainmap[__grains__['os']], name + ' restart') return not __salt__['cmd.retcode'](cmd) @@ -88,10 +96,7 @@ def status(name, sig=None): salt '*' service.status [service signature] ''' - sig = name if not sig else sig - cmd = "{0[ps]} | grep {1} | grep -v grep | awk '{{print $2}}'".format( - __grains__, sig) - return __salt__['cmd.run'](cmd).strip() + return __salt__['status.pid'](sig if sig else name) def reload(name): diff --git a/salt/modules/shadow.py b/salt/modules/shadow.py index ff4a6fd431..04655dc5ed 100644 --- a/salt/modules/shadow.py +++ b/salt/modules/shadow.py @@ -146,3 +146,15 @@ def set_warndays(name, warndays): if post_info['warn'] != pre_info['warn']: return post_info['warn'] == warndays return False + +def set_date(name, date): + ''' + sets the value for the date the password was last changed to the epoch (January 1, 1970). See man chage. + + CLI Example:: + + salt '*' shadow.set_date username 0 + ''' + cmd = 'chage -d {0} {1}'.format(date, name) + __salt__['cmd.run'](cmd) + \ No newline at end of file diff --git a/salt/modules/smf.py b/salt/modules/smf.py new file mode 100644 index 0000000000..36a7900f0e --- /dev/null +++ b/salt/modules/smf.py @@ -0,0 +1,177 @@ +''' +Service support for Solaris 10 and 11, should work with other systems +that use SMF also. (e.g. SmartOS) +''' + + +def __virtual__(): + ''' + Only work on systems which default to SMF + ''' + # Don't let this work on Solaris 9 since SMF doesn't exist on it. + enable = [ + 'Solaris', + ] + if __grains__['os'] in enable: + if __grains__['os'] == 'Solaris' and __grains__['kernelrelease'] == "5.9": + return False + return 'service' + return False + + +def get_enabled(): + ''' + Return the enabled services + + CLI Example:: + + salt '*' service.get_enabled + ''' + ret = set() + cmd = 'svcs -H -o SVC,STATE -s SVC' + lines = __salt__['cmd.run'](cmd).split('\n') + for line in lines: + comps = line.split() + if not comps: + continue + if 'online' in line: + ret.add(comps[0]) + return sorted(ret) + + +def get_disabled(): + ''' + Return the disabled services + + CLI Example:: + + salt '*' service.get_disabled + ''' + ret = set() + cmd = 'svcs -aH -o SVC,STATE -s SVC' + lines = __salt__['cmd.run'](cmd).split('\n') + for line in lines: + comps = line.split() + if not comps: + continue + if not 'online' in line and not 'legacy_run' in line: + ret.add(comps[0]) + return sorted(ret) + + +def get_all(): + ''' + Return all installed services + + CLI Example:: + + salt '*' service.get_all + ''' + ret = set() + cmd = 'svcs -aH -o SVC,STATE -s SVC' + lines = __salt__['cmd.run'](cmd).split('\n') + for line in lines: + comps = line.split() + if not comps: + continue + ret.add(comps[0]) + return sorted(ret) + + +def start(name): + ''' + Start the specified service + + CLI Example:: + + salt '*' service.start + ''' + cmd = '/usr/sbin/svcadm enable -t {0}'.format(name) + return not __salt__['cmd.retcode'](cmd) + + +def stop(name): + ''' + Stop the specified service + + CLI Example:: + + salt '*' service.stop + ''' + cmd = '/usr/sbin/svcadm disable -t {0}'.format(name) + return not __salt__['cmd.retcode'](cmd) + + +def restart(name): + ''' + Restart the named service + + CLI Example:: + + salt '*' service.restart + ''' + cmd = '/usr/sbin/svcadm restart {0}'.format(name) + return not __salt__['cmd.retcode'](cmd) + + +def status(name, sig=None): + ''' + Return the status for a service, returns a bool whether the service is + running. + + CLI Example:: + + salt '*' service.status + ''' + cmd = '/usr/bin/svcs -H -o STATE {0}'.format(name) + line = __salt__['cmd.run'](cmd).strip() + if line == 'online': + return True + else: + return False + + +def enable(name): + ''' + Enable the named service to start at boot + + CLI Example:: + + salt '*' service.enable + ''' + cmd = '/usr/sbin/svcadm enable {0}'.format(name) + return not __salt__['cmd.retcode'](cmd) + + +def disable(name): + ''' + Disable the named service to start at boot + + CLI Example:: + + salt '*' service.disable + ''' + cmd = '/usr/sbin/svcadm disable {0}'.format(name) + return not __salt__['cmd.retcode'](cmd) + + +def enabled(name): + ''' + Check to see if the named service is enabled to start on boot + + CLI Example:: + + salt '*' service.enabled + ''' + return name in get_enabled() + + +def disabled(name): + ''' + Check to see if the named service is disabled to start on boot + + CLI Example:: + + salt '*' service.disabled + ''' + return name in get_disabled() diff --git a/salt/modules/solr.py b/salt/modules/solr.py index f611c0f317..59b84684f4 100644 --- a/salt/modules/solr.py +++ b/salt/modules/solr.py @@ -967,8 +967,6 @@ def signal(signal=None): salt '*' solr.signal restart ''' - - ret = _get_return_dict() valid_signals = ('start', 'stop', 'restart') # Give a friendly error message for invalid signals @@ -979,7 +977,7 @@ def signal(signal=None): signal, ', '.join(msg)) cmd = "{0} {1}".format(__opts__['solr.init_script'], signal) - out = __salt__['cmd.run'](cmd) + __salt__['cmd.run'](cmd) def reload_core(host=None, core_name=None): diff --git a/salt/modules/ssh.py b/salt/modules/ssh.py index 62a9f4c627..7c07d1b55f 100644 --- a/salt/modules/ssh.py +++ b/salt/modules/ssh.py @@ -188,7 +188,6 @@ def auth_keys(user, config='.ssh/authorized_keys'): salt '*' ssh.auth_keys root ''' - ret = {} uinfo = __salt__['user.info'](user) full = os.path.join(uinfo['home'], config) if not os.path.isfile(full): @@ -270,7 +269,6 @@ def rm_auth_key(user, key, config='.ssh/authorized_keys'): # not an auth ssh key, perhaps a blank line continue - opts = ln.group(1) comps = ln.group(2).split() if len(comps) < 2: @@ -278,12 +276,6 @@ def rm_auth_key(user, key, config='.ssh/authorized_keys'): lines.append(line) continue - if opts: - # It has options, grab them - options = opts.split(',') - else: - options = [] - pkey = comps[1] if pkey == key: @@ -345,12 +337,14 @@ def set_auth_key( options=[], config='.ssh/authorized_keys'): ''' - Add a key to the authorized_keys file + Add a key to the authorized_keys file. The "key" parameter must only be the + string of text that is the encoded key. If the key begins with "ssh-rsa" + or ends with user@host, remove those from the key before passing it to this + function. CLI Example:: - salt '*' ssh.set_auth_key key='' enc='dsa'\ - comment='my key' options='[]' config='.ssh/authorized_keys' + salt '*' ssh.set_auth_key '' enc='dsa' ''' if len(key.split()) > 1: return 'invalid' diff --git a/salt/modules/state.py b/salt/modules/state.py index 34bc72a785..931175e154 100644 --- a/salt/modules/state.py +++ b/salt/modules/state.py @@ -162,7 +162,7 @@ def show_sls(mods, env='base', test=None, **kwargs): CLI Example:: - salt '*' state.sls core,edit.vim dev + salt '*' state.show_sls core,edit.vim dev ''' opts = copy.copy(__opts__) if not test is None: @@ -190,7 +190,7 @@ def show_masterstate(): return st_.compile_master() -def single(fun=None, test=None, **kwargs): +def single(fun, name, test=None, **kwargs): ''' Execute a single state function with the named kwargs, returns False if insufficient data is sent to the command @@ -199,17 +199,13 @@ def single(fun=None, test=None, **kwargs): salt '*' state.single pkg.installed name=vim ''' - if not 'name' in kwargs: - return False - if fun: - comps = fun.split('.') - if len(comps) < 2: - return False - else: - return False + comps = fun.split('.') + if len(comps) < 2: + return 'Invalid function passed' kwargs.update({'state': comps[0], 'fun': comps[1], - '__id__': kwargs['name']}) + '__id__': name, + 'name': name}) opts = copy.copy(__opts__) if not test is None: opts['test'] = test diff --git a/salt/modules/status.py b/salt/modules/status.py index e07ba5f07e..590bdeb963 100644 --- a/salt/modules/status.py +++ b/salt/modules/status.py @@ -86,7 +86,7 @@ def custom(): keys = opt.split('.') if keys[0] != 'status': continue - func = '%s()' % keys[1] + func = '{0}()'.format(keys[1]) vals = eval(func) for item in __opts__[opt]: @@ -443,3 +443,19 @@ def all_status(): 'uptime': uptime(), 'vmstats': vmstats(), 'w': w()} + + +def pid(sig): + ''' + Return the PID or an empty string if the process is running or not. + Pass a signature to use to find the process via ps. + + CLI Example:: + + salt '*' status.pid + ''' + cmd = "{0[ps]} | grep {1} | grep -v grep | awk '{{print $2}}'".format( + __grains__, sig) + return (__salt__['cmd.run_stdout'](cmd) or '').strip() + + diff --git a/salt/modules/supervisord.py b/salt/modules/supervisord.py new file mode 100644 index 0000000000..e78831bc3b --- /dev/null +++ b/salt/modules/supervisord.py @@ -0,0 +1,67 @@ +''' +Provide the service module for supervisord +''' + +from salt import exceptions, utils + + +def __virtual__(): + ''' + Check for supervisor. + ''' + try: + utils.check_or_die('supervisorctl') + except exceptions.CommandNotFoundError: + return False + + return 'supervisord' + + +def _ctl_cmd(cmd, name): + return 'supervisorctl {cmd} {name}'.format( + cmd=cmd, name=(name or '')) + + +def _get_return(ret): + if ret['retcode'] == 0: + return ret['stdout'] + else: + return '' + + +def start(name='all', user=None): + ''' + Start the named service + + CLI Example:: + salt '*' supervisord.start + ''' + ret = __salt__['cmd.run_all'](_ctl_cmd('start', name), runas=user) + return _get_return(ret) + + +def restart(name='all', user=None): + ''' + Restart the named service. + + CLI Example:: + salt '*' supervisord.restart + ''' + ret = __salt__['cmd.run_all'](_ctl_cmd('restart', name), runas=user) + return _get_return(ret) + + +def stop(name='all', user=None): + ''' + Stop the named service. + + CLI Example:: + salt '*' supervisord.stop + ''' + ret = __salt__['cmd.run_all'](_ctl_cmd('stop', name), runas=user) + return _get_return(ret) + + +def status(name=None, user=None): + ret = __salt__['cmd.run_all'](_ctl_cmd('status', name), runas=user) + return _get_return(ret) diff --git a/salt/modules/systemd.py b/salt/modules/systemd.py index 77d4a75f70..1fd489db91 100644 --- a/salt/modules/systemd.py +++ b/salt/modules/systemd.py @@ -1,9 +1,13 @@ ''' Provide the service module for systemd ''' +# Import Python libs +import re +# Import Salt libs +import salt.utils -import os - +VALID_UNIT_TYPES = ['service','socket', 'device', 'mount', 'automount', + 'swap', 'target', 'path', 'timer'] def __virtual__(): ''' @@ -16,6 +20,35 @@ def __virtual__(): return False +def _systemctl_cmd(action, name): + ''' + Build a systemctl command line. Treat unit names without one + of the valid suffixes as a service. + ''' + if not any(name.endswith(suffix) for suffix in VALID_UNIT_TYPES): + name += '.service' + return 'systemctl {0} {1}'.format(action, name) + +def _get_all_unit_files(): + ''' + Get all unit files and their state. Unit files ending in .service + are normalized so that they can be referenced without a type suffix. + ''' + rexp = re.compile('(?m)^(?P.+)\.(?P' + + '|'.join(VALID_UNIT_TYPES) + + ')\s+(?P.+)$') + + out = __salt__['cmd.run_stdout']('systemctl --no-legend list-unit-files | col -b') + + ret = {} + for match in rexp.finditer(out): + name = match.group('name') + if match.group('type') != 'service': + name += '.' + match.group('type') + ret[name] = match.group('state') + return ret + + def get_enabled(): ''' Return a list of all enabled services @@ -25,10 +58,9 @@ def get_enabled(): salt '*' service.get_enabled ''' ret = [] - for serv in get_all(): - cmd = 'systemctl is-enabled {0}.service'.format(serv) - if not __salt__['cmd.retcode'](cmd): - ret.append(serv) + for name, state in _get_all_unit_files().iteritems(): + if state == 'enabled': + ret.append(name) return sorted(ret) @@ -41,10 +73,9 @@ def get_disabled(): salt '*' service.get_disabled ''' ret = [] - for serv in get_all(): - cmd = 'systemctl is-enabled {0}.service'.format(serv) - if __salt__['cmd.retcode'](cmd): - ret.append(serv) + for name, state in _get_all_unit_files().iteritems(): + if state == 'disabled': + ret.append(name) return sorted(ret) @@ -56,14 +87,7 @@ def get_all(): salt '*' service.get_all ''' - ret = set() - sdir = '/lib/systemd/system' - if not os.path.isdir('/lib/systemd/system'): - return [] - for fn_ in os.listdir(sdir): - if fn_.endswith('.service'): - ret.add(fn_[:fn_.rindex('.')]) - return sorted(list(ret)) + return sorted(_get_all_unit_files().keys()) def start(name): @@ -74,8 +98,7 @@ def start(name): salt '*' service.start ''' - cmd = 'systemctl start {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + return not __salt__['cmd.retcode'](_systemctl_cmd('start', name)) def stop(name): @@ -86,8 +109,7 @@ def stop(name): salt '*' service.stop ''' - cmd = 'systemctl stop {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + return not __salt__['cmd.retcode'](_systemctl_cmd('stop', name)) def restart(name): @@ -98,8 +120,9 @@ def restart(name): salt '*' service.restart ''' - cmd = 'systemctl restart {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) + return not __salt__['cmd.retcode'](_systemctl_cmd('restart', name)) def reload(name): @@ -110,8 +133,7 @@ def reload(name): salt '*' service.reload ''' - cmd = 'systemctl reload {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + return not __salt__['cmd.retcode'](_systemctl_cmd('reload', name)) # The unused sig argument is required to maintain consistency in the state @@ -125,8 +147,7 @@ def status(name, sig=None): salt '*' service.status ''' - cmd = 'systemctl show {0}.service'.format(name) - ret = __salt__['cmd.run'](cmd) + ret = __salt__['cmd.run'](_systemctl_cmd('show', name)) index1 = ret.find('\nMainPID=') index2 = ret.find('\n', index1+9) mainpid = ret[index1+9:index2] @@ -143,8 +164,7 @@ def enable(name): salt '*' service.enable ''' - cmd = 'systemctl enable {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + return not __salt__['cmd.retcode'](_systemctl_cmd('enable', name)) def disable(name): @@ -155,8 +175,7 @@ def disable(name): salt '*' service.disable ''' - cmd = 'systemctl disable {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + return not __salt__['cmd.retcode'](_systemctl_cmd('disable', name)) def enabled(name): @@ -167,8 +186,7 @@ def enabled(name): salt '*' service.enabled ''' - cmd = 'systemctl is-enabled {0}.service'.format(name) - return not __salt__['cmd.retcode'](cmd) + return not __salt__['cmd.retcode'](_systemctl_cmd('is-enabled', name)) def disabled(name): @@ -179,5 +197,4 @@ def disabled(name): salt '*' service.disabled ''' - cmd = 'systemctl is-enabled {0}.service'.format(name) - return bool(__salt__['cmd.retcode'](cmd)) + return bool(__salt__['cmd.retcode'](_systemctl_cmd('is-enabled', name))) diff --git a/salt/modules/tomcat.py b/salt/modules/tomcat.py index aee85efc97..ff83dcca4c 100644 --- a/salt/modules/tomcat.py +++ b/salt/modules/tomcat.py @@ -1,7 +1,7 @@ ''' Support for Tomcat ''' - +# Import Python Libs import os @@ -25,7 +25,6 @@ def version(): ''' cmd = __catalina_home() + '/bin/catalina.sh version' out = __salt__['cmd.run'](cmd).split('\n') - ret = out[0].split(': ') for line in out: if not line: continue @@ -70,5 +69,7 @@ def signal(signal=None): if not valid_signals[signal]: return - cmd = __catalina_home() + '/bin/catalina.sh %s' % valid_signals[signal] - out = __salt__['cmd.run'](cmd) + cmd = '{0}/bin/catalina.sh {1}'.format( + __catalina_home(), valid_signals[signal] + ) + __salt__['cmd.run'](cmd) diff --git a/salt/modules/upstart.py b/salt/modules/upstart.py index 895312bdf4..a9a48402be 100644 --- a/salt/modules/upstart.py +++ b/salt/modules/upstart.py @@ -29,14 +29,14 @@ about this, at least. start on ((((filesystem and runlevel [!06]) and started dbus) and (drm-device-added card0 PRIMARY_DEVICE_FOR_DISPLAY=1 or stopped udev-fallback-graphics)) or runlevel PREVLEVEL=S) stop on runlevel [016] -DO NOT use this module on red hat systems, as red hat systems should use the +DO NOT use this module on Red Hat systems, as Red Hat systems should use the rh_service module, since red hat systems support chkconfig ''' - +# Import Python libs import glob import os - -from salt import utils +# Import salt libs +import salt.utils def __virtual__(): @@ -49,14 +49,73 @@ def __virtual__(): return False +def _find_utmp(): + ''' + Figure out which utmp file to use when determining runlevel. + Sometimes /var/run/utmp doesn't exist, /run/utmp is the new hotness. + ''' + result = {} + # These are the likely locations for the file on Ubuntu + for utmp in ('/var/run/utmp', '/run/utmp'): + try: + result[os.stat(utmp).st_mtime] = utmp + except: + pass + return result[sorted(result.keys()).pop()] + + +def _default_runlevel(): + ''' + Try to figure out the default runlevel. It is kept in + /etc/init/rc-sysinit.conf, but can be overridden with entries + in /etc/inittab, or via the kernel command-line at boot + ''' + # Try to get the "main" default. If this fails, throw up our + # hands and just guess "2", because things are horribly broken + try: + with open('/etc/init/rc-sysinit.conf') as fp_: + for line in fp_: + if line.startswith('env DEFAULT_RUNLEVEL'): + runlevel = line.split('=')[-1].strip() + except: + return '2' + + # Look for an optional "legacy" override in /etc/inittab + try: + with open('/etc/inittab') as fp_: + for line in fp_: + if not line.startswith('#') and 'initdefault' in line: + runlevel = line.split(':')[1] + except: + pass + + # The default runlevel can also be set via the kernel command-line. + # Kinky. + try: + valid_strings = set( + ('0', '1', '2', '3', '4', '5', '6', 's', 'S', '-s', 'single') + ) + with open('/proc/cmdline') as fp_: + for line in fp_: + for arg in line.strip().split(): + if arg in valid_strings: + runlevel = arg + break + except: + pass + + return runlevel + def _runlevel(): ''' Return the current runlevel - TODO: Should this return the "default" runlevel? For example, bad - things will likely happen when 'salt' is run in single-user mode. ''' - out = __salt__['cmd.run']('runlevel').strip() - return out.split()[1] + out = __salt__['cmd.run']('runlevel {0}'.format(_find_utmp())).strip() + try: + return out.split()[1] + except IndexError: + # The runlevel is unknown, return the default + return _default_runlevel() def _is_symlink(name): @@ -100,9 +159,7 @@ def _service_is_sysv(name): executable, like README or skeleton. ''' script = '/etc/init.d/{0}'.format(name) - if not _is_symlink(script): - return os.access(script, os.X_OK) - return False + return not _service_is_upstart(name) and os.access(script, os.X_OK) def _sysv_is_disabled(name): @@ -122,6 +179,23 @@ def _sysv_is_enabled(name): return not _sysv_is_disabled(name) +def _iter_service_names(): + ''' + Detect all of the service names available to upstart via init configuration + files and via classic sysv init scripts + ''' + found = set() + for line in glob.glob('/etc/init.d/*'): + name = os.path.basename(line) + found.add(name) + yield name + for line in glob.glob('/etc/init/*.conf'): + name = os.path.basename(line)[:-5] + if name in found: + continue + yield name + + def get_enabled(): ''' Return the enabled services @@ -131,8 +205,7 @@ def get_enabled(): salt '*' service.get_enabled ''' ret = set() - for line in glob.glob('/etc/init.d/*'): - name = line + for name in _iter_service_names(): if _service_is_upstart(name): if _upstart_is_enabled(name): ret.add(name) @@ -152,8 +225,7 @@ def get_disabled(): salt '*' service.get_disabled ''' ret = set() - for line in glob.glob('/etc/init.d/*'): - name = line + for name in _iter_service_names(): if _service_is_upstart(name): if _upstart_is_disabled(name): ret.add(name) @@ -207,10 +279,26 @@ def restart(name): salt '*' service.restart ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) cmd = 'service {0} restart'.format(name) return not __salt__['cmd.retcode'](cmd) +def full_restart(name): + ''' + Do a full restart (stop/start) of the named service + + CLI Example:: + + salt '*' service.full_restart + ''' + if name == 'salt-minion': + salt.utils.daemonize_if(__opts__) + cmd = 'service {0} --full-restart'.format(name) + return not __salt__['cmd.retcode'](cmd) + + def reload(name): ''' Reload the named service @@ -244,7 +332,7 @@ def _get_service_exec(): http://www.debian.org/doc/debian-policy/ch-opersys.html#s9.3.3 ''' executable = 'update-rc.d' - utils.check_or_die(executable) + salt.utils.check_or_die(executable) return executable diff --git a/salt/modules/useradd.py b/salt/modules/useradd.py index 096b311cd1..b874f0386d 100644 --- a/salt/modules/useradd.py +++ b/salt/modules/useradd.py @@ -101,7 +101,7 @@ def delete(name, remove=False, force=False): CLI Example:: - salt '*' user.delete name True True + salt '*' user.delete name remove=True force=True ''' cmd = 'userdel ' if remove: @@ -379,7 +379,9 @@ def list_groups(name): salt '*' user.list_groups foo ''' ugrp = set() - + # Add the primary user's group + ugrp.add(grp.getgrgid(pwd.getpwnam(name).pw_gid).gr_name) + # Now, all other groups the user belongs to for group in grp.getgrall(): if name in group.gr_mem: ugrp.add(group.gr_name) diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 5d22e677c9..eb8cab3d4b 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -223,7 +223,7 @@ def get_nics(vm_): # driver, source, and match can all have optional attributes if re.match('(driver|source|address)', v_node.tagName): temp = {} - for key in v_node.attributes: + for key in v_node.attributes.keys(): temp[key] = v_node.getAttribute(key) nic[str(v_node.tagName)] = temp # virtualport needs to be handled separately, to pick up the @@ -231,7 +231,7 @@ def get_nics(vm_): if v_node.tagName == "virtualport": temp = {} temp['type'] = v_node.getAttribute('type') - for key in v_node.attributes: + for key in v_node.attributes.keys(): temp[key] = v_node.getAttribute(key) nic['virtualport'] = temp if 'mac' not in nic: @@ -275,7 +275,7 @@ def get_graphics(vm_): for node in doc.getElementsByTagName("domain"): g_nodes = node.getElementsByTagName("graphics") for g_node in g_nodes: - for key in g_node.attributes: + for key in g_node.attributes.keys(): out[key] = g_node.getAttribute(key) return out @@ -301,7 +301,7 @@ def get_disks(vm_): target = targets[0] else: continue - if 'dev' in list(target.attributes) and 'file' in list(source.attributes): + if target.attributes.has_key('dev') and source.attributes.has_key('file'): disks[target.getAttribute('dev')] = { 'file': source.getAttribute('file')} for dev in disks: @@ -315,6 +315,65 @@ def get_disks(vm_): return disks +def setmem(vm_, memory, config=False): + ''' + Changes the amount of memory allocated to VM. The VM must be shutdown + for this to work. + + memory is to be specified in MB + If config is True then we ask libvirt to modify the config as well + + CLI Example:: + + salt '*' virt.setmem myvm 768 + ''' + if vm_state(vm_) != 'shutdown': + return False + + dom = _get_dom(vm_) + + # libvirt has a funny bitwise system for the flags in that the flag + # to affect the "current" setting is 0, which means that to set the + # current setting we have to call it a second time with just 0 set + flags = libvirt.VIR_DOMAIN_MEM_MAXIMUM + if config: + flags = flags | libvirt.VIR_DOMAIN_AFFECT_CONFIG + + ret1 = dom.setMemoryFlags(memory * 1024, flags) + ret2 = dom.setMemoryFlags(memory * 1024, libvirt.VIR_DOMAIN_AFFECT_CURRENT) + + # return True if both calls succeeded + return ret1 == ret2 == 0 + + +def setvcpus(vm_, vcpus, config=False): + ''' + Changes the amount of vcpus allocated to VM. The VM must be shutdown + for this to work. + + vcpus is an int representing the number to be assigned + If config is True then we ask libvirt to modify the config as well + + CLI Example:: + + salt '*' virt.setvcpus myvm 2 + ''' + if vm_state(vm_) != 'shutdown': + return False + + dom = _get_dom(vm_) + + # see notes in setmem + flags = libvirt.VIR_DOMAIN_VCPU_MAXIMUM + if config: + flags = flags | libvirt.VIR_DOMAIN_AFFECT_CONFIG + + ret1 = dom.setVcpusFlags(vcpus, flags) + ret2 = dom.setVcpusFlags(vcpus, libvirt.VIR_DOMAIN_AFFECT_CURRENT) + + return ret1 == ret2 == 0 + + def freemem(): ''' Return an int representing the amount of memory that has not been given @@ -388,8 +447,7 @@ def shutdown(vm_): salt '*' virt.shutdown ''' dom = _get_dom(vm_) - dom.shutdown() - return True + return dom.shutdown() == 0 def pause(vm_): @@ -401,8 +459,7 @@ def pause(vm_): salt '*' virt.pause ''' dom = _get_dom(vm_) - dom.suspend() - return True + return dom.suspend() == 0 def resume(vm_): @@ -414,8 +471,7 @@ def resume(vm_): salt '*' virt.resume ''' dom = _get_dom(vm_) - dom.resume() - return True + return dom.resume() == 0 def create(vm_): @@ -427,8 +483,7 @@ def create(vm_): salt '*' virt.create ''' dom = _get_dom(vm_) - dom.create() - return True + return dom.create() == 0 def start(vm_): @@ -442,6 +497,49 @@ def start(vm_): return create(vm_) +def reboot(vm_): + ''' + Reboot a domain via ACPI request + + CLI Example:: + + salt '*' virt.reboot + ''' + dom = _get_dom(vm_) + + # reboot has a few modes of operation, passing 0 in means the + # hypervisor will pick the best method for rebooting + return dom.reboot(0) == 0 + + +def reset(vm_): + ''' + Reset a VM by emulating the reset button on a physical machine + + CLI Example:: + + salt '*' virt.reset + ''' + dom = _get_dom(vm_) + + # reset takes a flag, like reboot, but it is not yet used + # so we just pass in 0 + # see: http://libvirt.org/html/libvirt-libvirt.html#virDomainReset + return dom.reset(0) == 0 + + +def ctrl_alt_del(vm_): + ''' + Sends CTRL+ALT+DEL to a VM + + CLI Example:: + + salt '*' virt.ctrl_alt_del + ''' + dom = _get_dom(vm_) + return dom.sendKey(0, 0, [29, 56, 111], 3, 0) == 0 + + def create_xml_str(xml): ''' Start a domain based on the xml passed to the function @@ -451,8 +549,7 @@ def create_xml_str(xml): salt '*' virt.create_xml_str ''' conn = __get_conn() - conn.createXML(xml, 0) - return True + return conn.createXML(xml, 0) is not None def create_xml_path(path): @@ -581,12 +678,8 @@ def destroy(vm_): salt '*' virt.destroy ''' - try: - dom = _get_dom(vm_) - dom.destroy() - except Exception: - return False - return True + dom = _get_dom(vm_) + return dom.destroy() == 0 def undefine(vm_): @@ -598,12 +691,8 @@ def undefine(vm_): salt '*' virt.undefine ''' - try: - dom = _get_dom(vm_) - dom.undefine() - except Exception: - return False - return True + dom = _get_dom(vm_) + return dom.undefine() == 0 def purge(vm_, dirs=False): @@ -617,7 +706,8 @@ def purge(vm_, dirs=False): salt '*' virt.purge ''' disks = get_disks(vm_) - destroy(vm_) + if not destroy(vm_): + return False directories = set() for disk in disks: os.remove(disks[disk]['file']) diff --git a/salt/modules/virtualenv.py b/salt/modules/virtualenv.py index 265653d586..4c37a66a1c 100644 --- a/salt/modules/virtualenv.py +++ b/salt/modules/virtualenv.py @@ -5,7 +5,9 @@ Create virtualenv environments from salt import utils -__opts__ = {} +__opts__ = { + 'venv_bin': 'virtualenv' +} __pillar__ = {} @@ -70,4 +72,4 @@ def create(path, ' --prompt {0}'.format(prompt) if prompt else '']), path=path) - return __salt__['cmd.run'](cmd, runas=runas) + return __salt__['cmd.run_all'](cmd, runas=runas) diff --git a/salt/modules/win_file.py b/salt/modules/win_file.py index bf2f26a756..22dd71e28e 100644 --- a/salt/modules/win_file.py +++ b/salt/modules/win_file.py @@ -230,9 +230,9 @@ def get_sum(path, form='md5'): try: return getattr(hashlib, form)(open(path, 'rb').read()).hexdigest() except (IOError, OSError) as e: - return 'File Error: %s' % (str(e)) + return 'File Error: {0}'.format(e) except AttributeError as e: - return 'Hash ' + form + ' not supported' + return 'Hash {0} not supported'.format(form) except NameError as e: return 'Hashlib unavailable - please fix your python install' except Exception as e: @@ -242,7 +242,7 @@ def get_sum(path, form='md5'): def find(path, **kwargs): ''' Approximate the Unix find(1) command and return a list of paths that - meet the specified critera. + meet the specified criteria. The options include match criteria:: @@ -544,10 +544,10 @@ def touch(name, atime=None, mtime=None): else: times = (atime, mtime) os.utime(name, times) - except TypeError as exc: + except TypeError: msg = "atime and mtime must be integers" raise SaltInvocationError(msg) - except (IOError, OSError) as exc: + except (IOError, OSError): return False return os.path.exists(name) diff --git a/salt/modules/win_network.py b/salt/modules/win_network.py index 3ba7424203..30200e2c4b 100644 --- a/salt/modules/win_network.py +++ b/salt/modules/win_network.py @@ -13,6 +13,7 @@ __outputter__ = { 'netstat': 'txt', } + def __virtual__(): ''' Only works on Windows systems @@ -27,7 +28,7 @@ def _sanitize_host(host): ''' Sanitize host string. ''' - return "".join([ + return ''.join([ c for c in host[0:255] if c in (ascii_letters + digits + '.-') ]) @@ -40,7 +41,7 @@ def ping(host): salt '*' network.ping archlinux.org ''' - cmd = 'ping -n 4 %s' % _sanitize_host(host) + cmd = 'ping -n 4 {0}'.format(_sanitize_host(host)) return __salt__['cmd.run'](cmd) @@ -81,7 +82,7 @@ def traceroute(host): salt '*' network.traceroute archlinux.org ''' ret = [] - cmd = 'tracert %s' % _sanitize_host(host) + cmd = 'tracert {0}'.format(_sanitize_host(host)) lines = __salt__['cmd.run'](cmd).split('\n') for line in lines: if not ' ' in line: @@ -133,7 +134,7 @@ def nslookup(host): salt '*' network.nslookup archlinux.org ''' ret = [] - cmd = 'nslookup %s' % _sanitize_host(host) + cmd = 'nslookup {0}'.format(_sanitize_host(host)) lines = __salt__['cmd.run'](cmd).split('\n') for line in lines: if line.startswith('Non-authoritative'): @@ -154,7 +155,7 @@ def dig(host): salt '*' network.dig archlinux.org ''' - cmd = 'dig %s' % _sanitize_host(host) + cmd = 'dig {0}'.format(_sanitize_host(host)) return __salt__['cmd.run'](cmd) @@ -170,7 +171,7 @@ def _cidr_to_ipv4_netmask(cidr_bits): netmask += '255' cidr_bits -= 8 else: - netmask += '%d' % (256-(2**(8-cidr_bits))) + netmask += '{0:d}'.format(256-(2**(8-cidr_bits))) cidr_bits = 0 return netmask @@ -204,7 +205,7 @@ def _interfaces_ipconfig(out): iface['inet'] = list() addr = {'address': val.rstrip('(Preferred)'), 'netmask': None, - 'broadcast': None} # TODO find the broadcast + 'broadcast': None} # TODO find the broadcast iface['inet'].append(addr) elif 'IPv6 Address' in key: if 'inet6' not in iface: diff --git a/salt/modules/win_service.py b/salt/modules/win_service.py index f2bb06e192..5b6b227d7d 100644 --- a/salt/modules/win_service.py +++ b/salt/modules/win_service.py @@ -111,7 +111,7 @@ def restart(name): salt '*' service.restart ''' stopcmd = 'sc stop "{0}"'.format(name) - stopped = __salt__['cmd.run'](stopcmd) + __salt__['cmd.run'](stopcmd) servicestate = status(name) while True: servicestate = status(name) diff --git a/salt/modules/win_useradd.py b/salt/modules/win_useradd.py index cbc5e7c193..925c9dc060 100644 --- a/salt/modules/win_useradd.py +++ b/salt/modules/win_useradd.py @@ -228,7 +228,6 @@ def getent(): salt '*' user.getent ''' ret = [] - items = {} users = [] startusers = False cmd = 'net user' diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index d9301a2564..28a6029b2d 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -8,7 +8,7 @@ try: import rpm from rpmUtils.arch import getBaseArch has_yumdeps = True -except ImportError: +except (ImportError, AttributeError): has_yumdeps = False import logging @@ -24,7 +24,7 @@ def __virtual__(): # Return this for pkg on RHEL/Fedora based distros that ship with python # 2.6 or greater. - dists = ('CentOS', 'Scientific', 'RedHat') + dists = ('CentOS', 'Scientific', 'RedHat', 'CloudLinux') if __grains__['os'] == 'Fedora': if int(__grains__['osrelease'].split('.')[0]) >= 11: return 'pkg' diff --git a/salt/modules/yumpkg5.py b/salt/modules/yumpkg5.py index 059edcb88b..149efe79c8 100644 --- a/salt/modules/yumpkg5.py +++ b/salt/modules/yumpkg5.py @@ -13,7 +13,7 @@ def __virtual__(): ''' # Return this for pkg on RHEL/Fedora based distros that do not ship with # python 2.6 or greater. - dists = ('CentOS', 'Scientific', 'RedHat') + dists = ('CentOS', 'Scientific', 'RedHat', 'CloudLinux') if __grains__['os'] == 'Fedora': if int(__grains__['osrelease'].split('.')[0]) < 11: return 'pkg' @@ -134,7 +134,7 @@ def refresh_db(): salt '*' pkg.refresh_db ''' cmd = 'yum -q clean dbcache' - retcode = __salt__['cmd.retcode'](cmd) + __salt__['cmd.retcode'](cmd) return True @@ -171,7 +171,7 @@ def install(pkg, refresh=False, repo='', skip_verify=False, **kwargs): if refresh: refresh_db() - retcode = __salt__['cmd.retcode'](cmd) + __salt__['cmd.retcode'](cmd) new = list_pkgs() pkgs = {} for npkg in new: @@ -205,7 +205,7 @@ def upgrade(): ''' old = list_pkgs() cmd = 'yum -q -y upgrade' - retcode = __salt__['cmd.retcode'](cmd) + __salt__['cmd.retcode'](cmd) new = list_pkgs() pkgs = {} for npkg in new: @@ -236,7 +236,7 @@ def remove(pkg): ''' old = list_pkgs() cmd = 'yum -q -y remove ' + pkg - retcode = __salt__['cmd.retcode'](cmd) + __salt__['cmd.retcode'](cmd) new = list_pkgs() return _list_removed(old, new) diff --git a/salt/modules/zfs.py b/salt/modules/zpool.py similarity index 96% rename from salt/modules/zfs.py rename to salt/modules/zpool.py index 174296eb4f..516fc74c77 100644 --- a/salt/modules/zfs.py +++ b/salt/modules/zpool.py @@ -11,7 +11,8 @@ def __virtual__(): ''' FreeBSD only for now ''' - return 'zfs' if __grains__['os'] == 'FreeBSD' else False + enabled = ['FreeBSD', 'Solaris'] + return 'zpool' if __grains__['os'] in enabled else False def list_installed(): ''' @@ -55,7 +56,7 @@ def create_file_vdevice(size, *names): ''' creates file based ``virtual devices`` for a zpool - *names is a list of full paths for mkfile to create + ``*names`` is a list of full paths for mkfile to create CLI Example:: @@ -77,7 +78,7 @@ def create_file_vdevice(size, *names): devs = ' '.join(l) cmd = 'mkfile {0} {1}'.format(size, devs) - res = __salt__['cmd.run'](cmd) + __salt__['cmd.run'](cmd) # Makesure the files are there for name in names: @@ -148,7 +149,7 @@ def zpool_destory(pool_name): ret = {} if pool_exists(pool_name): cmd = 'zpool destroy {0}'.format(pool_name) - res = __salt__['cmd.run'](cmd) + __salt__['cmd.run'](cmd) if not pool_exists(pool_name): ret[pool_name] = "Deleted" return ret diff --git a/salt/modules/zypper.py b/salt/modules/zypper.py index 64fe5f3be7..d192481c78 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -7,7 +7,7 @@ def __virtual__(): ''' Set the virtual pkg module if the os is openSUSE ''' - return 'pkg' if __grains__['os_family'] == 'Suse' else False + return 'pkg' if __grains__.get('os_family', '') == 'Suse' else False def _list_removed(old, new): diff --git a/salt/output.py b/salt/output.py index 180dcd52bf..ec4391cfce 100644 --- a/salt/output.py +++ b/salt/output.py @@ -21,32 +21,52 @@ import salt.utils from salt._compat import string_types from salt.exceptions import SaltException -__all__ = ('get_outputter',) +__all__ = ('get_outputter', 'get_printout') log = logging.getLogger(__name__) +def strip_clean(returns): + ''' + Check for the state_verbose option and strip out the result=True + and changes={} members of the state return list. + ''' + rm_tags = [] + for tag in returns: + if returns[tag]['result'] and not returns[tag]['changes']: + rm_tags.append(tag) + for tag in rm_tags: + returns.pop(tag) + return returns + +def get_printout(ret, out, opts, indent=None): + ''' + Return the proper printout + ''' + if isinstance(ret, list) or isinstance(ret, dict): + if opts['raw_out']: + return get_outputter('raw') + elif opts['json_out']: + printout = get_outputter('json') + if printout and indent is not None: + printout.indent = indent + return printout + elif opts.get('text_out', False): + return get_outputter('txt') + elif opts['yaml_out']: + return get_outputter('yaml') + elif out is not None: + return get_outputter(out) + return None + # Pretty print any salt exceptions + elif isinstance(ret, SaltException): + return get_outputter('txt') def display_output(ret, out, opts): ''' Display the output of a command in the terminal ''' - if isinstance(ret, list) or isinstance(ret, dict): - if opts['raw_out']: - printout = get_outputter('raw') - elif opts['json_out']: - printout = get_outputter('json') - elif opts['txt_out']: - printout = get_outputter('txt') - elif opts['yaml_out']: - printout = get_outputter('yaml') - elif out: - printout = get_outputter(out) - else: - printout = get_outputter(None) - # Pretty print any salt exceptions - elif isinstance(ret, SaltException): - printout = get_outputter("txt") - printout(ret) + printout = get_printout(ret, out, opts) + printout(ret, color=not bool(opts['no_color']), **opts) class Outputter(object): @@ -88,6 +108,10 @@ class HighStateOutputter(Outputter): hstrs.append(('{0}----------\n {1}{2[ENDC]}' .format(hcolor, err, colors))) if isinstance(data[host], dict): + # Strip out the result: True, without changes returns if + # state_verbose is False + if not kwargs.get('state_verbose', False): + data[host] = strip_clean(data[host]) # Verify that the needed data is present for tname, info in data[host].items(): if not '__run_num__' in info: @@ -111,6 +135,20 @@ class HighStateOutputter(Outputter): hcolor = colors['YELLOW'] tcolor = colors['YELLOW'] comps = tname.split('_|-') + if kwargs.get('state_output', 'full').lower() == 'terse': + # Print this chunk in a terse way and continue in the + # loop + msg = (' {0}Name: {1} - Function: {2} - Result: {3}{4}' + ).format( + tcolor, + comps[2], + comps[-1], + str(ret['result']), + colors['ENDC'] + ) + hstrs.append(msg) + continue + hstrs.append(('{0}----------\n State: - {1}{2[ENDC]}' .format(tcolor, comps[0], colors))) hstrs.append(' {0}Name: {1}{2[ENDC]}'.format( @@ -198,7 +236,7 @@ class JSONOutputter(Outputter): # A good kwarg might be: indent=4 if 'color' in kwargs: kwargs.pop('color') - ret = json.dumps(data, **kwargs) + ret = json.dumps(data) except TypeError: log.debug(traceback.format_exc()) # Return valid json for unserializable objects @@ -215,7 +253,7 @@ class YamlOutputter(Outputter): def __call__(self, data, **kwargs): if 'color' in kwargs: kwargs.pop('color') - print(yaml.dump(data, **kwargs)) + print(yaml.dump(data)) def get_outputter(name=None): diff --git a/salt/payload.py b/salt/payload.py index 3e338da9ce..5dfb7ae50b 100644 --- a/salt/payload.py +++ b/salt/payload.py @@ -120,12 +120,14 @@ class SREQ(object): ''' Create a generic interface to wrap salt zeromq req calls. ''' - def __init__(self, master, serial='msgpack', linger=0): + def __init__(self, master, id_='', serial='msgpack', linger=0): self.master = master self.serial = Serial(serial) context = zmq.Context() self.socket = context.socket(zmq.REQ) self.socket.linger = linger + if id_: + self.socket.setsockopt(zmq.IDENTITY, id_) self.socket.connect(master) def send(self, enc, load, tries=1, timeout=60): diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index 51b41939e8..8a40848423 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -7,7 +7,6 @@ import os import copy import collections import logging -import subprocess # Import Salt libs import salt.loader @@ -17,10 +16,6 @@ import salt.crypt from salt._compat import string_types from salt.template import compile_template -# Import third party libs -import zmq -import yaml - log = logging.getLogger(__name__) @@ -242,12 +237,6 @@ class Pillar(object): for item in data: if isinstance(item, string_types): matches[env].append(item) - ext_matches = self.client.ext_nodes() - for env in ext_matches: - if env in matches: - matches[env] = list(set(ext_matches[env]).union(matches[env])) - else: - matches[env] = ext_matches[env] return matches def render_pstate(self, sls, env, mods): @@ -337,7 +326,7 @@ class Pillar(object): ext.update(self.ext_pillars[key](*val)) else: ext.update(self.ext_pillars[key](val)) - except Exception as e: + except Exception: log.critical('Failed to load ext_pillar {0}'.format(key)) return ext diff --git a/salt/pillar/cmd_json.py b/salt/pillar/cmd_json.py new file mode 100644 index 0000000000..bca0471c7a --- /dev/null +++ b/salt/pillar/cmd_json.py @@ -0,0 +1,26 @@ +''' +Execute a command and read the output as JSON. The JSON data is then directly +overlaid onto the minion's pillar data +''' + +# Import python libs +import logging + +# Import third party libs +import json + +# Set up logging +log = logging.getLogger(__name__) + + +def ext_pillar(command): + ''' + Execute a command and read the output as JSON + ''' + try: + return json.loads(__salt__['cmd.run'](command)) + except Exception: + log.critical( + 'JSON data from {0} failed to parse'.format(command) + ) + return {} diff --git a/salt/pillar/mongo.py b/salt/pillar/mongo.py new file mode 100644 index 0000000000..45dc5b4d7b --- /dev/null +++ b/salt/pillar/mongo.py @@ -0,0 +1,143 @@ +""" +Read pillar data from a mongodb collection. + +This module will load a node-specific pillar dictionary from a mongo +collection. It uses the node's id for lookups and can load either the whole +document, or just a specific field from that +document as the pillar dictionary. + +Salt Master Mongo Configuration +=============================== + +The module shares the same base mongo connection variables as +:py:mod:`salt.returners.mongo_return`. These variables go in your master +config file. + + * ``mongo.db`` - The mongo database to connect to. Defaults to ``'salt'``. + * ``mongo.host`` - The mongo host to connect to. Supports replica sets by + specifying all hosts in the set, comma-delimited. Defaults to ``'salt'``. + * ``mongo.port`` - The port that the mongo database is running on. Defaults + to ``27017``. + * ``mongo.user`` - The username for connecting to mongo. Only required if + you are using mongo authentication. Defaults to ``''``. + * ``mongo.password`` - The password for connecting to mongo. Only required + if you are using mongo authentication. Defaults to ``''``. + + +Configuring the Mongo ext_pillar +================================ + +The Mongo ext_pillar takes advantage of the fact that the Salt Master +configuration file is yaml. It uses a sub-dictionary of values to adjust +specific features of the pillar. This is the explicit single-line dictionary +notation for yaml. One may be able to get the easier-to-read multine dict to +work correctly with some experimentation. + +.. code-block:: yaml + + ext_pillar: + - mongo: {collection: vm, id_field: name, re_pattern: \.example\.com, fields: [customer_id, software, apache_vhosts]} + +In the example above, we've decided to use the ``vm`` collection in the +database to store the data. Minion ids are stored in the ``name`` field on +documents in that collection. And, since minon ids are FQDNs in most cases, +we'll need to trim the domain name in order to find the minon by hostname in +the collection. When we find a minion, return only the ``customer_id``, +``software``, and ``apache_vhosts`` fields, as that will contain the data we +want for a given node. They will be available directly inside the ``pillar`` +dict in your SLS templates. + + +Module Documentation +==================== +""" + +# Import python libs +import logging +import re + +try: + import pymongo + has_pymongo = True +except ImportError: + has_pymongo = False + + +__opts__ = {'mongo.db': 'salt', + 'mongo.host': 'salt', + 'mongo.password': '', + 'mongo.port': 27017, + 'mongo.user': ''} + + +def __virtual__(): + if not has_pymongo: + return False + return 'mongo' + +# Set up logging +log = logging.getLogger(__name__) + + +def ext_pillar(collection='pillar', id_field='_id', re_pattern=None, + re_replace='', fields=None): + """ + Connect to a mongo database and read per-node pillar information. + + Parameters: + * `collection`: The mongodb collection to read data from. Defaults to + ``'pillar'``. + * `id_field`: The field in the collection that represents an individual + minon id. Defaults to ``'_id'``. + * `re_pattern`: If your naming convention in the collection is shorter + than the minion id, you can use this to trim the name. + `re_pattern` will be used to match the name, and `re_replace` will + be used to replace it. Backrefs are supported as they are in the + Python standard library. If ``None``, no mangling of the name will + be performed - the collection will be searched with the entire + minion id. Defaults to ``None``. + * `re_replace`: Use as the replacement value in node ids matched with + `re_pattern`. Defaults to ''. Feel free to use backreferences here. + * `fields`: The specific fields in the document to use for the pillar + data. If ``None``, will use the entire document. If using the + entire document, the ``_id`` field will be converted to string. Be + careful with other fields in the document as they must be string + serializable. Defaults to ``None``. + """ + conn = pymongo.Connection(__opts__['mongo.host'], __opts__['mongo.port']) + db = conn[__opts__['mongo.db']] + + user = __opts__.get('mongo.user') + password = __opts__.get('mongo.password') + + if user and password: + db.authenticate(user, password) + + # Do the regex string replacement on the minion id + minion_id = __opts__['id'] + if re_pattern: + minion_id = re.sub(re_pattern, re_replace, minion_id) + + log.info("ext_pillar.mongo: looking up pillar def for {'%s': '%s'} " + "in mongo" % (id_field, minion_id)) + + + pillar = db[collection].find_one({id_field: minion_id}, fields=fields) + if pillar: + if fields: + log.debug("ext_pillar.mongo: found document, returning fields '%s'" + % fields) + else: + log.debug("ext_pillar.mongo: found document, returning whole doc") + if '_id' in pillar: + # Converting _id to a string + # will avoid the most common serialization error cases, but DBRefs + # and whatnot will still cause problems. + pillar['_id'] = str(pillar['_id']) + return pillar + else: + # If we can't find the minion the database it's not necessarily an + # error. + log.debug("ext_pillar.mongo: no document found in collection %s" % + collection) + return {} diff --git a/salt/renderers/json_mako.py b/salt/renderers/json_mako.py index 1fe7ef4889..dc4a122012 100644 --- a/salt/renderers/json_mako.py +++ b/salt/renderers/json_mako.py @@ -31,5 +31,13 @@ def render(template_file, env='', sls=''): sls=sls) if not tmp_data.get('result', False): raise SaltRenderError(tmp_data.get('data', - 'Unknown render error in yaml_mako renderer')) - return json.loads(tmp_data['data']) + 'Unknown render error in json_mako renderer')) + + # Ensure that we're not passing lines with a shebang in the JSON. + to_return = [] + for line in tmp_data['data'].split('\n'): + if line and "#!" not in line: + to_return.append(line) + to_return = '\n'.join(to_return) + + return json.loads(to_return) diff --git a/salt/renderers/py.py b/salt/renderers/py.py index e88103d97b..af54168fc3 100644 --- a/salt/renderers/py.py +++ b/salt/renderers/py.py @@ -31,6 +31,6 @@ def render(template, env='', sls=''): sls=sls) if not tmp_data.get('result', False): raise SaltRenderError(tmp_data.get('data', - 'Unknown render error in yaml_jinja renderer')) + 'Unknown render error in py renderer')) return tmp_data['data'] diff --git a/salt/returners/cassandra_return.py b/salt/returners/cassandra_return.py index 941085ac8e..75211645b8 100644 --- a/salt/returners/cassandra_return.py +++ b/salt/returners/cassandra_return.py @@ -53,7 +53,7 @@ def returner(ret): 'id': ret['id']} if isinstance(ret['return'], dict): for key, value in ret['return'].items(): - columns['return.%s' % (key,)] = str(value) + columns['return.{0}'.format(key)] = str(value) else: columns['return'] = str(ret['return']) diff --git a/salt/returners/mongo_return.py b/salt/returners/mongo_return.py index 8f4d83135b..c85d262a75 100644 --- a/salt/returners/mongo_return.py +++ b/salt/returners/mongo_return.py @@ -2,6 +2,18 @@ Return data to a mongodb server Required python modules: pymongo + + +This returner will send data from the minions to a MongoDB server. To +configure the settings for your MongoDB server, add the following lines +to the minion config files:: + + mongo.db: + mongo.host: + mongo.user: + mongo.password: + mongo.port: 27017 + ''' import logging @@ -15,6 +27,7 @@ except ImportError: log = logging.getLogger(__name__) +# These options should be overridden in the minion config file __opts__ = {'mongo.db': 'salt', 'mongo.host': 'salt', 'mongo.password': '', diff --git a/salt/runner.py b/salt/runner.py index ee768f9c3b..6ce0f0a909 100644 --- a/salt/runner.py +++ b/salt/runner.py @@ -43,7 +43,7 @@ class Runner(object): ''' Execute the runner sequence ''' - if self.opts['doc']: + if self.opts.get('doc', False): self._print_docs() else: self._verify_fun() diff --git a/salt/runners/jobs.py b/salt/runners/jobs.py index 7943b2997f..57307e6642 100644 --- a/salt/runners/jobs.py +++ b/salt/runners/jobs.py @@ -1,5 +1,5 @@ ''' -A conveniance system to manage jobs, both active and already run +A convenience system to manage jobs, both active and already run ''' # Import Python Modules @@ -22,7 +22,6 @@ def active(): perspective ''' ret = {} - job_dir = os.path.join(__opts__['cachedir'], 'jobs') client = salt.client.LocalClient(__opts__['conf_file']) active_ = client.cmd('*', 'saltutil.running', timeout=__opts__['timeout']) for minion, data in active_.items(): @@ -57,7 +56,7 @@ def active(): def lookup_jid(jid): ''' - Return the printout from a previousely executed job + Return the printout from a previously executed job ''' def _format_ret(full_ret): ''' @@ -79,7 +78,9 @@ def lookup_jid(jid): ret = formatted[0] out = formatted[1] else: - ret = SaltException('Job {0} hasn\'t finished. No data yet :('.format(jid)) + ret = SaltException( + 'Job {0} hasn\'t finished. No data yet :('.format(jid) + ) out = '' # Determine the proper output method and run it @@ -117,3 +118,38 @@ def list_jobs(): 'Target-type': load['tgt_type']} print(yaml.dump(ret)) return ret + +def print_job(job_id): + ''' + Print job available details, including return data. + ''' + serial = salt.payload.Serial(__opts__) + ret = {} + job_dir = os.path.join(__opts__['cachedir'], 'jobs') + for top in os.listdir(job_dir): + t_path = os.path.join(job_dir, top) + for final in os.listdir(t_path): + loadpath = os.path.join(t_path, final, '.load.p') + if not os.path.isfile(loadpath): + continue + load = serial.load(open(loadpath, 'rb')) + jid = load['jid'] + if job_id == jid: + hosts_path = os.path.join(t_path, final) + hosts_return = {} + for host in os.listdir(hosts_path): + host_path = os.path.join(hosts_path, host) + if os.path.isdir(host_path): + returnfile = os.path.join(host_path, 'return.p') + if not os.path.isfile(returnfile): + continue + return_data = serial.load(open(returnfile, 'rb')) + hosts_return[host] = return_data + ret[jid] = {'Start Time': salt.utils.jid_to_time(jid), + 'Function': load['fun'], + 'Arguments': list(load['arg']), + 'Target': load['tgt'], + 'Target-type': load['tgt_type'], + 'Result': hosts_return} + salt.output.get_outputter('yaml')(ret) + return ret diff --git a/salt/scripts.py b/salt/scripts.py index fc3a425938..37815de852 100644 --- a/salt/scripts.py +++ b/salt/scripts.py @@ -1,8 +1,11 @@ ''' This module contains the function calls to execute command line scipts ''' +# Import Python libs import os +import sys +# Import Salt libs import salt import salt.cli @@ -19,6 +22,8 @@ def salt_minion(): ''' Kick off a salt minion daemon. ''' + if '' in sys.path: + sys.path.remove('') minion = salt.Minion() minion.start() @@ -63,6 +68,8 @@ def salt_call(): Directly call a salt command in the modules, does not require a running salt minion to run. ''' + if '' in sys.path: + sys.path.remove('') try: client = salt.cli.SaltCall() client.run() @@ -74,6 +81,8 @@ def salt_run(): ''' Execute a salt convenience routine. ''' + if '' in sys.path: + sys.path.remove('') try: client = salt.cli.SaltRun() client.run() @@ -86,6 +95,8 @@ def salt_main(): Publish commands to the salt system from the command line on the master. ''' + if '' in sys.path: + sys.path.remove('') try: client = salt.cli.SaltCMD() client.run() diff --git a/salt/state.py b/salt/state.py index 70a8b55440..4cbe2664d6 100644 --- a/salt/state.py +++ b/salt/state.py @@ -20,9 +20,6 @@ import logging import collections import traceback -# Import Third Party libs -import zmq - # Import Salt libs import salt.utils import salt.loader @@ -246,7 +243,7 @@ class State(object): Check to see if the modules for this state instance need to be updated, only update if the state is a file. If the function is managed check to see if the file is a possible module type, e.g. a - python, pyx, or .so. Always refresh if the function is recuse, + python, pyx, or .so. Always refresh if the function is recurse, since that can lay down anything. ''' def _refresh(): @@ -267,21 +264,6 @@ class State(object): elif data['state'] == 'pkg': _refresh() - def format_verbosity(self, returns): - ''' - Check for the state_verbose option and strip out the result=True - and changes={} members of the state return list. - ''' - if self.opts['state_verbose']: - return returns - rm_tags = [] - for tag in returns: - if returns[tag]['result'] and not returns[tag]['changes']: - rm_tags.append(tag) - for tag in rm_tags: - returns.pop(tag) - return returns - def verify_data(self, data): ''' Verify the data, return an error statement if something is wrong @@ -300,13 +282,6 @@ class State(object): errors.append(err) if errors: return errors - if data['fun'].startswith('mod_'): - errors.append( - 'State {0} in sls {1} uses an invalid function {2}'.format( - data['state'], - data['__sls__'], - data['fun']) - ) full = data['state'] + '.' + data['fun'] if full not in self.states: if '__sls__' in data: @@ -386,7 +361,7 @@ class State(object): errors.append(err) if not isinstance(body, dict): err = ('The type {0} in {1} is not formated as a dictionary' - .format(name, body['__sls__'])) + .format(name, body)) errors.append(err) continue for state in body: @@ -676,6 +651,42 @@ class State(object): high[name][state].append(arg) return high, errors + def apply_exclude(self, high): + ''' + Read in the __exclude__ list and remove all excluded objects from the + high data + ''' + if '__exclude__' not in high: + return high + ex_sls = set() + ex_id = set() + exclude = high.pop('__exclude__') + for exc in exclude: + if isinstance(exc, str): + # The exclude statement is a string, assume it is an sls + ex_sls.add(exc) + if isinstance(exc, dict): + # Explicitly declared exclude + if len(exc) != 1: + continue + key = exc.keys()[0] + if key == 'sls': + ex_sls.add(exc['sls']) + elif key == 'id': + ex_id.add(exc['id']) + # Now the excludes have been simplified, use them + if ex_sls: + # There are sls excludes, find the associtaed ids + for name, body in high.items(): + if name.startswith('__'): + continue + if body.get('__sls__', '') in ex_sls: + ex_id.add(name) + for id_ in ex_id: + if id_ in high: + high.pop(id_) + return high + def requisite_in(self, high): ''' Extend the data reference with requisite_in arguments @@ -684,6 +695,8 @@ class State(object): req_in_all = req_in.union(set(['require', 'watch'])) extend = {} for id_, body in high.items(): + if not isinstance(body, dict): + continue for state, run in body.items(): if state.startswith('__'): continue @@ -755,6 +768,11 @@ class State(object): continue if next(iter(arg)) in ignore_args: continue + # Don't use name or names + if arg.keys()[0] == 'name': + continue + if arg.keys()[0] == 'names': + continue extend[ext_id][_state].append(arg) continue if key == 'use': @@ -776,6 +794,11 @@ class State(object): continue if next(iter(arg)) in ignore_args: continue + # Don't use name or names + if arg.keys()[0] == 'name': + continue + if arg.keys()[0] == 'names': + continue extend[id_][state].append(arg) continue found = False @@ -807,6 +830,22 @@ class State(object): Call a state directly with the low data structure, verify data before processing. ''' + errors = self.verify_data(data) + if errors: + ret = { + 'result': False, + 'name': data['name'], + 'changes': {}, + 'comment': '', + } + for err in errors: + ret['comment'] += '{0}\n'.format(err) + ret['__run_num__'] = self.__run_num + self.__run_num += 1 + format_log(ret) + self.module_refresh(data) + return ret + log.info( 'Executing state {0[state]}.{0[fun]} for {0[name]}'.format( data @@ -835,6 +874,7 @@ class State(object): format_log(ret) if 'provider' in data: self.load_modules() + self.module_refresh(data) return ret def call_chunks(self, chunks): @@ -870,13 +910,18 @@ class State(object): ''' present = False if 'watch' in low: - present = True + if not '{0}.mod_watch'.format(low['state']) in self.states: + if 'require' in low: + low['require'].extend(low.pop('watch')) + else: + low['require'] = low.pop('watch') + else: + present = True if 'require' in low: present = True if not present: return 'met' reqs = {'require': [], 'watch': []} - status = 'unmet' for r_state in reqs: if r_state in low: for req in low[r_state]: @@ -993,7 +1038,6 @@ class State(object): ''' Process a high data call and ensure the defined states. ''' - err = [] errors = [] # If there is extension data reconcile it high, ext_errors = self.reconcile_extend(high) @@ -1003,28 +1047,112 @@ class State(object): return errors high, req_in_errors = self.requisite_in(high) errors += req_in_errors + high = self.apply_exclude(high) # Verify that the high data is structurally sound if errors: return errors # Compile and verify the raw chunks chunks = self.compile_high_data(high) - errors += self.verify_chunks(chunks) # If there are extensions in the highstate, process them and update # the low data chunks if errors: return errors - ret = self.format_verbosity(self.call_chunks(chunks)) + ret = self.call_chunks(chunks) return ret + def render_template(self, high, template): + errors = [] + if not high: + return high, errors + + if not isinstance(high, dict): + errors.append( + 'Template {0} does not render to a dictionary'.format(template) + ) + return high, errors + + invalid_items = ('include', 'exclude', 'extends') + for item in invalid_items: + if item in high: + errors.append( + 'The \'{0}\' declaration found on \'{1}\' is invalid when ' + 'rendering single templates'.format(item, template) + ) + return high, errors + + for name in high: + if not isinstance(high[name], dict): + if isinstance(high[name], string_types): + # Is this is a short state, it needs to be padded + if '.' in high[name]: + comps = high[name].split('.') + high[name] = { + #'__sls__': template, + #'__env__': None, + comps[0]: [comps[1]] + } + continue + + errors.append( + 'Name {0} in template {1} is not a dictionary'.format( + name, template + ) + ) + continue + skeys = set() + for key in sorted(high[name]): + if key.startswith('_'): + continue + if not isinstance(high[name][key], list): + continue + if '.' in key: + comps = key.split('.') + # Salt doesn't support state files such as: + # + # /etc/redis/redis.conf: + # file.managed: + # - source: salt://redis/redis.conf + # - user: redis + # - group: redis + # - mode: 644 + # file.comment: + # - regex: ^requirepass + # + # XXX: Bad example here since no features requiring a + # master should be here. + if comps[0] in skeys: + errors.append( + 'Name \'{0}\' in template \'{1}\' contains ' + 'multiple state decs of the same type'.format( + name, template + ) + ) + continue + high[name][comps[0]] = high[name].pop(key) + high[name][comps[0]].append(comps[1]) + skeys.add(comps[0]) + continue + skeys.add(key) + + #if '__sls__' not in high[name]: + # high[name]['__sls__'] = template + #if '__env__' not in high[name]: + # high[name]['__env__'] = None + + return high, errors + def call_template(self, template): ''' Enforce the states in a template ''' high = compile_template( template, self.rend, self.opts['renderer']) - if high: - return self.call_high(high) - return high + if not high: + return high + high, errors = self.render_template(high, template) + if errors: + return errors + return self.call_high(high) def call_template_str(self, template): ''' @@ -1032,9 +1160,12 @@ class State(object): ''' high = compile_template_str( template, self.rend, self.opts['renderer']) - if high: - return self.call_high(high) - return high + if not high: + return high + high, errors = self.render_template(high, '') + if errors: + return errors + return self.call_high(high) class BaseHighState(object): @@ -1348,10 +1479,22 @@ class BaseHighState(object): state['__extend__'] = [ext] else: state['__extend__'].append(ext) + if 'exclude' in state: + exc = state.pop('exclude') + if not isinstance(exc, list): + err = ('Exclude Declaration in SLS {0} is not formed ' + 'as a list'.format(sls)) + errors.append(err) + if '__exclude__' not in state: + state['__exclude__'] = exc + else: + state['__exclude__'].extend(exc) for name in state: if not isinstance(state[name], dict): if name == '__extend__': continue + if name == '__exclude__': + continue if isinstance(state[name], string_types): # Is this is a short state, it needs to be padded @@ -1366,7 +1509,7 @@ class BaseHighState(object): .format(name, sls))) continue skeys = set() - for key in state[name]: + for key in sorted(state[name]): if key.startswith('_'): continue if not isinstance(state[name][key], list): @@ -1417,6 +1560,8 @@ class BaseHighState(object): # The extend members can not be treated as globally unique: if '__extend__' in state and '__extend__' in highstate: highstate['__extend__'].extend(state.pop('__extend__')) + if '__exclude__' in state and '__exclude__' in highstate: + highstate['__exclude__'].extend(state.pop('__exclude__')) for id_ in state: if id_ in highstate: if highstate[id_] != state[id_]: @@ -1485,9 +1630,12 @@ class BaseHighState(object): ''' Return just the highstate or the errors ''' + err = [] top = self.get_top() + err += self.verify_tops(top) matches = self.top_matches(top) high, errors = self.render_highstate(matches) + err += errors if errors: return errors @@ -1499,7 +1647,6 @@ class BaseHighState(object): Compile the highstate but don't run it, return the low chunks to see exactly what the highstate will execute ''' - err = [] top = self.get_top() matches = self.top_matches(top) high, errors = self.render_highstate(matches) @@ -1511,12 +1658,12 @@ class BaseHighState(object): # Verify that the high data is structurally sound errors += self.state.verify_high(high) - # Compile and verify the raw chunks - chunks = self.state.compile_high_data(high) - errors += self.state.verify_chunks(chunks) - if errors: return errors + + # Compile and verify the raw chunks + chunks = self.state.compile_high_data(high) + return chunks diff --git a/salt/states/cmd.py b/salt/states/cmd.py index 3ba3542335..7118b4ccb6 100644 --- a/salt/states/cmd.py +++ b/salt/states/cmd.py @@ -38,7 +38,6 @@ import copy # Import salt libs from salt.exceptions import CommandExecutionError -import salt.utils.templates def _run_check(cmd_kwargs, onlyif, unless, cwd, user, group, shell): @@ -315,12 +314,16 @@ def script(name, 'cwd': cwd, 'template': template}) - # Changet the source to be the name arg if it is nto specified + run_check_cmd_kwargs = {'cwd': cwd, + 'runas': user, + 'shell': shell or __grains__['shell'], } + + # Change the source to be the name arg if it is not specified if source is None: source = name try: - cret = _run_check(cmd_kwargs, onlyif, unless, cwd, user, group, shell) + cret = _run_check(run_check_cmd_kwargs, onlyif, unless, cwd, user, group, shell) if isinstance(cret, dict): ret.update(cret) return ret diff --git a/salt/states/file.py b/salt/states/file.py index 89875100c5..652554d6fe 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -66,7 +66,7 @@ takes a few arguments: Recursive directory management can also be set via the ``recurse`` function. Recursive directory management allows for a directory on the salt master to be recursively copied down to the minion. This is a great tool for -deploying large code and configuration systems. A recuse state would look +deploying large code and configuration systems. A recurse state would look something like this: .. code-block:: yaml @@ -78,6 +78,7 @@ something like this: # Import Python libs from contextlib import nested # For < 2.7 compat import os +import errno import shutil import difflib import hashlib @@ -141,23 +142,56 @@ def _makedirs(path, user=None, group=None, mode=None): directory = os.path.dirname(path) if not os.path.isdir(directory): - os.makedirs(directory) + # turn on the executable bits for user, group and others. + # Note: the special bits are set to 0. + if mode: + mode = int(mode[-3:], 8) | 0111 + + _makedirs_perms(directory, user, group, mode) # If a caller such as managed() is invoked with # makedirs=True, make sure that any created dirs # are created with the same user and group to # follow the principal of least surprise method. - nmode = '' - if mode: - for char in mode: - if char == '0': - nmode += char - elif int(char) % 2 == 1: - # Is executable, continue - nmode += char - else: - # The mode is even, it need an executable bit - nmode += str(int(char) + 1) - _check_perms(directory, None, user, group, nmode) + + + +def _makedirs_perms(name, user=None, group=None, mode=0755): + ''' + Taken and modified from os.makedirs to set user, group and mode for each + directory created. + ''' + path = os.path + mkdir = os.mkdir + head, tail = path.split(name) + if not tail: + head, tail = path.split(head) + if head and tail and not path.exists(head): + try: + _makedirs_perms(head, user, group, mode) + except OSError, e: + # be happy if someone already created the path + if e.errno != errno.EEXIST: + raise + if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists + return + mkdir(name) + _check_perms(name, None, user, group, int("%o" % mode) if mode else None) + + +def _check_user(user, group): + ''' + Checks if the named user and group are present on the minion + ''' + err = '' + if user: + uid = __salt__['file.user_to_uid'](user) + if uid == '': + err += 'User {0} is not available '.format(user) + if group: + gid = __salt__['file.group_to_gid'](group) + if gid == '': + err += 'Group {0} is not available'.format(group) + return err def _is_bin(path): @@ -214,6 +248,8 @@ def _clean_dir(root, keep): real_keep.add(root) if isinstance(keep, list): for fn_ in keep: + if not os.path.isabs(fn_): + continue real_keep.add(fn_) while True: fn_ = os.path.dirname(fn_) @@ -250,6 +286,7 @@ def _source_list(source, source_hash, env): if isinstance(source, list): # get the master file list mfiles = __salt__['cp.list_master'](env) + mdirs = __salt__['cp.list_master_dirs'](env) for single in source: if isinstance(single, dict): # check the proto, if it is http or ftp then download the file @@ -273,7 +310,7 @@ def _source_list(source, source_hash, env): source_hash = single_hash break elif isinstance(single, string_types): - if single in mfiles: + if single[7:] in mfiles or single[7:] in mdirs: source = single break return source, source_hash @@ -379,26 +416,40 @@ def _get_managed( def _check_perms(name, ret, user, group, mode): ''' Check the permissions on files and chown if needed + + Note: 'mode' here is expected to be either a string or an integer, + in which case it will be converted into a base-10 string. + + What this means is that in your YAML salt file, you can specify + mode as an integer(eg, 644) or as a string(eg, '644'). But, to + specify mode 0777, for example, it must be specified as the string, + '0777' otherwise, 0777 will be parsed as an octal and you'd get 511 + instead. ''' if not ret: ret = {'name': name, 'changes': {}, - 'comment': '', - 'result': False} + 'comment': [], + 'result': True} + orig_comment = '' + else: + orig_comment = ret['comment'] + ret['comment'] = [] + # Check permissions perms = {} perms['luser'] = __salt__['file.get_user'](name) perms['lgroup'] = __salt__['file.get_group'](name) - perms['lmode'] = __salt__['file.get_mode'](name).lstrip('0') + perms['lmode'] = str(__salt__['file.get_mode'](name)).lstrip('0') # Mode changes if needed if mode: - if mode != perms['lmode']: + if str(mode) != perms['lmode']: if not __opts__['test']: __salt__['file.set_mode'](name, mode) - if mode != __salt__['file.get_mode'](name).lstrip('0'): + if str(mode) != __salt__['file.get_mode'](name).lstrip('0'): ret['result'] = False - ret['comment'] += 'Failed to change mode to {0} '.format(mode) + ret['comment'].append('Failed to change mode to {0}'.format(mode)) else: ret['changes']['mode'] = mode # user/group changes if needed, then check if it worked @@ -414,28 +465,64 @@ def _check_perms(name, ret, user, group, mode): user = perms['luser'] if group is None: group = perms['lgroup'] - __salt__['file.chown']( - name, - user, - group - ) + try: + __salt__['file.chown']( + name, + user, + group + ) + except OSError: + ret['result'] = False + if user: if user != __salt__['file.get_user'](name): ret['result'] = False - ret['comment'] = 'Failed to change user to {0} '.format(user) + ret['comment'].append('Failed to change user to {0}'.format(user)) elif 'cuser' in perms: ret['changes']['user'] = user if group: if group != __salt__['file.get_group'](name): ret['result'] = False - ret['comment'] += ('Failed to change group to {0} ' + ret['comment'].append('Failed to change group to {0}' .format(group)) elif 'cgroup' in perms: ret['changes']['group'] = group + if isinstance(orig_comment, basestring): + if orig_comment: + ret['comment'].insert(0, orig_comment) + ret['comment'] = '; '.join(ret['comment']) return ret, perms +def _get_recurse_dest(prefix, fn_, source, env): + ''' + Return the destination path to copy the file path, fn_(as returned by + a call to __salt__['cp.cache_dir']), to. + ''' + local_roots = [] + if __opts__['file_client'] == 'local': + local_roots = __opts__['file_roots'][env] + local_roots.sort(key=lambda p: len(p), reverse=True) + + srcpath = source[7:] # the path after "salt://" + + # in solo mode(ie, file_client=='local'), fn_ is a path below + # a file root; in remote mode, fn_ is a path below the cache_dir. + for root in local_roots: + n = len(root) + # if root is the longest prefix path of fn_ + if root == fn_[:n]: + cachedir = os.path.join(root, srcpath) + break + else: + cachedir = os.path.join( + __opts__['cachedir'], 'files', env, srcpath) + + return os.path.join(prefix, os.path.relpath(fn_, cachedir)) + + + def _check_recurse( name, source, @@ -456,17 +543,7 @@ def _check_recurse( for fn_ in __salt__['cp.cache_dir'](source, env, include_empty): if not fn_.strip(): continue - dest = os.path.join(name, - os.path.relpath( - fn_, - os.path.join( - __opts__['cachedir'], - 'files', - env, - source[7:] - ) - ) - ) + dest = _get_recurse_dest(name, fn_, source, env) dirname = os.path.dirname(dest) if not dirname in vdir: # verify the directory perms if they are set @@ -526,9 +603,14 @@ def _check_directory( if not 'group' in recurse: group = None for root, dirs, files in os.walk(name): - for name in files: - path = os.path.join(root, name) - fchange = _check_dir_meta(path, user, group, None) + for fname in files: + fchange = {} + path = os.path.join(root, fname) + stats = __salt__['file.stats'](path, 'md5') + if not user is None and user != stats['user']: + fchange['user'] = user + if not group is None and group != stats['group']: + fchange['group'] = group if fchange: changes[path] = fchange for name in dirs: @@ -536,16 +618,13 @@ def _check_directory( fchange = _check_dir_meta(path, user, group, None) if fchange: changes[path] = fchange - else: - if not os.path.isdir(name): - changes[name] = 'new' - return None, 'A new directory is set to be made at {0}'.format( - name) + if not os.path.isdir(name): + changes[name] = {'directory': 'new'} if changes: comment = 'The following files will be changed:\n' for fn_ in changes: for key, val in changes[fn_].items(): - comment += '{0}: {1} - {2}'.format(fn_, key, val) + comment += '{0}: {1} - {2}\n'.format(fn_, key, val) return None, comment return True, 'The directory {0} is in the correct state'.format(name) @@ -650,7 +729,7 @@ def _check_file_meta( slines = src.readlines() nlines = name_.readlines() changes['diff'] = ( - ''.join(difflib.unified_diff(nlines, slines)) + ''.join(difflib.unified_diff(slines, nlines)) ) else: changes['sum'] = 'Checksum differs' @@ -671,7 +750,7 @@ def _check_touch(name, atime, mtime): Check to see if a file needs to be updated or created ''' if not os.path.exists(name): - return None, 'File {0} is set to be created' + return None, 'File {0} is set to be created'.format(name) stats = __salt__['file.stats'](name) if not atime is None: if str(atime) != str(stats['atime']): @@ -821,6 +900,26 @@ def absent(name): return ret +def exists(name): + ''' + Verify that the named file or directory is present or exists. + Ensures pre-requisites outside of salts per-vue have been previously + satisified (aka, keytabs, private keys, etc.) before deployment + + name + Absolute path which must exist + ''' + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': ''} + if not os.path.exists(name): + return _error(ret, ('Specified path {0} does not exist').format(name)) + + ret['comment'] = 'Path {0} exists'.format(name) + return ret + + def managed(name, source=None, source_hash='', @@ -882,8 +981,9 @@ def managed(name, file. replace - If this file should be replaced, if false then this command will - be ignored if the file exists already. Default is true. + If this file should be replaced. If false, this command will + not overwrite file contents but will enforce permissions if the file + exists already. Default is true. context Overrides default context variables passed to the template. @@ -900,6 +1000,10 @@ def managed(name, 'comment': '', 'name': name, 'result': True} + u_check = _check_user(user, group) + if u_check: + # The specified user or group do not exist + return _error(ret, u_check) if not os.path.isabs(name): return _error( ret, ('Specified file {0} is not an absolute' @@ -914,7 +1018,13 @@ def managed(name, if not replace: if os.path.exists(name): - ret['comment'] = 'File {0} exists. No changes made'.format(name) + # Check and set the permissions if necessary + ret, perms = _check_perms(name, ret, user, group, mode) + if __opts__['test']: + ret['comment'] = 'File {0} not updated'.format(name) + elif not ret['changes'] and ret['result']: + ret['comment'] = ('File {0} exists with proper permissions.' + ' No changes made.').format(name) return ret if not source: return touch(name, makedirs=makedirs) @@ -1151,6 +1261,10 @@ def directory(name, 'changes': {}, 'result': True, 'comment': ''} + u_check = _check_user(user, group) + if u_check: + # The specified user or group do not exist + return _error(ret, u_check) if not os.path.isabs(name): return _error( ret, 'Specified file {0} is not an absolute path'.format(name)) @@ -1338,7 +1452,12 @@ def recurse(name, ret = {'name': name, 'changes': {}, 'result': True, - 'comment': ''} + 'comment': {} # { path: [comment, ...] } + } + u_check = _check_user(user, group) + if u_check: + # The specified user or group do not exist + return _error(ret, u_check) if not os.path.isabs(name): return _error( ret, 'Specified file {0} is not an absolute path'.format(name)) @@ -1352,7 +1471,9 @@ def recurse(name, # it is not a dir, but it exists - fail out return _error( ret, 'The path {0} exists and is not a directory'.format(name)) - os.makedirs(name) + _makedirs_perms(name, user, group, + int(str(dir_mode), 8) if dir_mode else None) + if __opts__['test']: ret['result'], ret['comment'] = _check_recurse( name, @@ -1366,36 +1487,42 @@ def recurse(name, env, include_empty) return ret + + def update_changes_by_perms(path, mode, changetype='updated'): + _ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': [] + } + _check_perms(path, _ret, user, group, mode) + ret['result'] &= _ret['result'] # ie, once false, stay false. + if _ret['comment']: + comments = ret['comment'].setdefault(path, []) + comments.extend(_ret['comment']) + if _ret['changes']: + ret['changes'][path] = changetype + + # If source is a list, find which in the list actually exists + source, source_hash = _source_list(source, '', env) + vdir = set() for fn_ in __salt__['cp.cache_dir'](source, env, include_empty): if not fn_.strip(): continue - dest = os.path.join(name, - os.path.relpath( - fn_, - os.path.join( - __opts__['cachedir'], - 'files', - env, - source[7:] - ) - ) - ) + # fn_ here is the absolute source path of the file to copy from; + # it is either a normal file or an empty dir(if include_empthy==true). + + dest = _get_recurse_dest(name, fn_, source, env) dirname = os.path.dirname(dest) + keep.add(dest) if not os.path.isdir(dirname): _makedirs(dest, user=user, group=group) if not dirname in vdir: # verify the directory perms if they are set - # _check_perms(name, ret, user, group, mode) - _ret, perms = _check_perms(dirname, {}, user, group, dir_mode) - if _ret['changes']: - ret['changes'][dirname] = 'updated' + update_changes_by_perms(dirname, dir_mode) vdir.add(dirname) if os.path.isfile(dest): - _ret, perms = _check_perms(dest, {}, user, group, file_mode) - if _ret['changes']: - ret['changes'][dest] = 'updated' - keep.add(dest) + update_changes_by_perms(dest, file_mode) srch = '' dsth = '' # The file is present, if the sum differes replace it @@ -1404,35 +1531,28 @@ def recurse(name, dsth = hashlib.md5(dst_.read()).hexdigest() if srch != dsth: # The downloaded file differes, replace! - # FIXME: no metadata (ownership, permissions) available salt.utils.copyfile( fn_, dest, _backup_mode(backup), __opts__['cachedir']) - ret['changes'][dest] = 'updated' + update_changes_by_perms(dest, file_mode) elif os.path.isdir(dest) and include_empty: #check perms - _ret, perms = _check_perms(dest, {}, user, group, dir_mode) - if _ret['changes']: - ret['changes'][dest] = 'updated' - keep.add(dest) + update_changes_by_perms(dest, dir_mode) else: - keep.add(dest) if os.path.isdir(fn_) and include_empty: #create empty dir os.mkdir(dest) - - if dir_mode: - __salt__['file.set_mode'](dest, dir_mode) + update_changes_by_perms(dest, dir_mode) else: # The destination file is not present, make it - # FIXME: no metadata (ownership, permissions) available salt.utils.copyfile( fn_, dest, _backup_mode(backup), __opts__['cachedir']) + update_changes_by_perms(dest, file_mode) ret['changes'][dest] = 'new' keep = list(keep) if clean: @@ -1501,15 +1621,23 @@ def sed(name, before, after, limit='', backup='.bak', options='-r -e', ret['comment'] = 'File {0} is set to be updated'.format(name) ret['result'] = None return ret + with open(name, 'rb') as fp_: + slines = fp_.readlines() # should be ok now; perform the edit __salt__['file.sed'](name, before, after, limit, backup, options, flags) + with open(name, 'rb') as fp_: + nlines = fp_.readlines() # check the result ret['result'] = __salt__['file.contains_regex'](name, after) + if slines != nlines: + # Changes happened, add them + ret['changes']['diff'] = ( + ''.join(difflib.unified_diff(slines, nlines)) + ) if ret['result']: ret['comment'] = 'File successfully edited' - ret['changes'].update({'old': before, 'new': after}) else: ret['comment'] = 'Expected edit does not appear in file' @@ -1518,6 +1646,27 @@ def sed(name, before, after, limit='', backup='.bak', options='-r -e', def comment(name, regex, char='#', backup='.bak'): ''' + Comment out specified lines in a file. + + path + The full path to the file to be edited + regex + A regular expression used to find the lines that are to be commented; + this pattern will be wrapped in parenthesis and will move any + preceding/trailing ``^`` or ``$`` characters outside the parenthesis + (e.g., the pattern ``^foo$`` will be rewritten as ``^(foo)$``) + char : ``#`` + The character to be inserted at the beginning of a line in order to + comment it out + backup : ``.bak`` + The file will be backed up before edit with this file extension + + .. warning:: + + This backup will be overwritten each time ``sed`` / ``comment`` / + ``uncomment`` is called. Meaning the backup will only be useful + after the first invocation. + Usage:: /etc/fstab: @@ -1547,16 +1696,25 @@ def comment(name, regex, char='#', backup='.bak'): ret['comment'] = 'File {0} is set to be updated'.format(name) ret['result'] = None return ret + with open(name, 'rb') as fp_: + slines = fp_.readlines() # Perform the edit __salt__['file.comment'](name, regex, char, backup) + with open(name, 'rb') as fp_: + nlines = fp_.readlines() + # Check the result ret['result'] = __salt__['file.contains_regex'](name, unanchor_regex) + if slines != nlines: + # Changes happened, add them + ret['changes']['diff'] = ( + ''.join(difflib.unified_diff(slines, nlines)) + ) + if ret['result']: ret['comment'] = 'Commented lines successfully' - ret['changes'] = {'old': '', - 'new': 'Commented lines matching: {0}'.format(regex)} else: ret['comment'] = 'Expected commented lines not found' @@ -1565,6 +1723,23 @@ def comment(name, regex, char='#', backup='.bak'): def uncomment(name, regex, char='#', backup='.bak'): ''' + Uncomment specified commented lines in a file + + path + The full path to the file to be edited + regex + A regular expression used to find the lines that are to be uncommented. + This regex should not include the comment character. A leading ``^`` + character will be stripped for convenience (for easily switching + between comment() and uncomment()). + char : ``#`` + The character to remove in order to uncomment a line; if a single + whitespace character follows the comment it will also be removed + backup : ``.bak`` + The file will be backed up before edit with this file extension; + **WARNING:** each time ``sed``/``comment``/``uncomment`` is called will + overwrite this backup + Usage:: /etc/adduser.conf: @@ -1579,8 +1754,6 @@ def uncomment(name, regex, char='#', backup='.bak'): if not check_res: return _error(ret, check_msg) - unanchor_regex = regex.lstrip('^') - # Make sure the pattern appears in the file if __salt__['file.contains_regex'](name, regex): ret['comment'] = 'Pattern already uncommented' @@ -1596,23 +1769,34 @@ def uncomment(name, regex, char='#', backup='.bak'): ret['comment'] = 'File {0} is set to be updated'.format(name) ret['result'] = None return ret + + with open(name, 'rb') as fp_: + slines = fp_.readlines() + # Perform the edit __salt__['file.uncomment'](name, regex, char, backup) + with open(name, 'rb') as fp_: + nlines = fp_.readlines() + # Check the result ret['result'] = __salt__['file.contains_regex'](name, regex) + if slines != nlines: + # Changes happened, add them + ret['changes']['diff'] = ( + ''.join(difflib.unified_diff(slines, nlines)) + ) + if ret['result']: ret['comment'] = 'Uncommented lines successfully' - ret['changes'] = {'old': '', - 'new': 'Uncommented lines matching: {0}'.format(regex)} else: ret['comment'] = 'Expected uncommented lines not found' return ret -def append(name, text): +def append(name, text=None, makedirs=False, source=None, source_hash=None): ''' Ensure that some text appears at the end of a file @@ -1640,35 +1824,81 @@ def append(name, text): ''' ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''} + if makedirs: + dirname = os.path.dirname(name) + if not __salt__['file.directory_exists'](dirname): + _makedirs(name) + check_res, check_msg = _check_directory( + dirname, None, None, False, None, False, False + ) + if not check_res: + return _error(ret, check_msg) + + # Make sure that we have a file + __salt__['file.touch'](name) + check_res, check_msg = _check_file(name) if not check_res: return _error(ret, check_msg) + if source: + # get cached file or copy it to cache + cached_source_path = __salt__['cp.cache_file'](source) + logger.debug( + "state file.append cached source {0} -> {1}".format( + source, cached_source_path + ) + ) + cached_source = managed( + cached_source_path, source=source, source_hash=source_hash + ) + if cached_source['result'] is True: + logger.debug( + "state file.append is loading text contents from cached source " + "{0}({1})".format(source, cached_source_path) + ) + text = open(cached_source_path, 'r').read() + if isinstance(text, string_types): text = (text,) + with open(name, 'rb') as fp_: + slines = fp_.readlines() + + count = 0 + for chunk in text: + + if __salt__['file.contains_regex']( + name, salt.utils.build_whitepace_splited_regex(chunk)): + continue + try: lines = chunk.split('\n') except AttributeError: - logger.debug('Error appending text to %s; given object is: %s', - name, type(chunk)) + logger.debug( + 'Error appending text to {0}; given object is: {1}'.format( + name, type(chunk) + ) + ) return _error(ret, 'Given text is not a string') for line in lines: - if __salt__['file.contains'](name, line): - continue - else: - if __opts__['test']: - ret['comment'] = 'File {0} is set to be updated'.format( - name) - ret['result'] = None - return ret - __salt__['file.append'](name, line) - cgs = ret['changes'].setdefault('new', []) - cgs.append(line) + if __opts__['test']: + ret['comment'] = 'File {0} is set to be updated'.format(name) + ret['result'] = None + return ret + __salt__['file.append'](name, line) + count += 1 - count = len(ret['changes'].get('new', [])) + with open(name, 'rb') as fp_: + nlines = fp_.readlines() + + if slines != nlines: + # Changes happened, add them + ret['changes']['diff'] = ( + ''.join(difflib.unified_diff(slines, nlines)) + ) ret['comment'] = 'Appended {0} lines'.format(count) ret['result'] = True @@ -1693,7 +1923,8 @@ def touch(name, atime=None, mtime=None, makedirs=False): } if not os.path.isabs(name): return _error( - ret, 'Specified file {0} is not an absolute path'.format(name)) + ret, 'Specified file {0} is not an absolute path'.format(name) + ) if __opts__['test']: ret['result'], ret['comment'] = _check_touch(name, atime, mtime) @@ -1703,7 +1934,8 @@ def touch(name, atime=None, mtime=None, makedirs=False): _makedirs(name) if not os.path.isdir(os.path.dirname(name)): return _error( - ret, 'Directory not present to touch file {0}'.format(name)) + ret, 'Directory not present to touch file {0}'.format(name) + ) exists = os.path.exists(name) ret['result'] = __salt__['file.touch'](name, atime, mtime) @@ -1711,7 +1943,9 @@ def touch(name, atime=None, mtime=None, makedirs=False): ret['comment'] = 'Created empty file {0}'.format(name) ret['changes']['new'] = name elif exists and ret['result']: - ret['comment'] = 'Updated times on file {0}'.format(name) + ret['comment'] = 'Updated times on {0} {1}'.format( + 'directory' if os.path.isdir(name) else 'file', name + ) return ret diff --git a/salt/states/git.py b/salt/states/git.py index 2f748d7b23..87c34855f1 100644 --- a/salt/states/git.py +++ b/salt/states/git.py @@ -5,6 +5,9 @@ Interaction with Git repositories. NOTE: This modules is under heavy development and the API is subject to change. It may be replaced with a generic VCS module if this proves viable. +Important, before using git over ssh, make sure your remote host fingerprint +exists in "~/.ssh/known_hosts" file. + .. code-block:: yaml https://github.com/saltstack/salt.git: @@ -71,13 +74,13 @@ def latest(name, ('Repository {0} update is probably required (current ' 'revision is {1})').format(target, current_rev)) - __salt__['git.pull'](target, user=runas) + pull_out = __salt__['git.pull'](target, user=runas) if rev: __salt__['git.checkout'](target, rev, user=runas) if submodules: - __salt__['git.submodule'](target, user=runas) + __salt__['git.submodule'](target, user=runas, opts='--recursive') new_rev = __salt__['git.revision'](cwd=target, user=runas) if current_rev != new_rev: @@ -87,6 +90,13 @@ def latest(name, ret['comment'] = 'Repository {0} updated'.format(target) ret['changes']['revision'] = '{0} => {1}'.format( current_rev, new_rev) + else: + # Check that there was no error preventing the revision update + if 'error:' in pull_out: + return _fail( + ret, + 'An error was thrown by git:\n{0}'.format(pull_out) + ) else: if os.path.isdir(target): # git clone is required, but target exists -- however it is empty diff --git a/salt/states/hg.py b/salt/states/hg.py new file mode 100644 index 0000000000..621ca16638 --- /dev/null +++ b/salt/states/hg.py @@ -0,0 +1,160 @@ +''' +Interaction with Mercurial repositories. +======================================== + +NOTE: This module is currently experimental. Most of this code is copied from +git.py with changes to handle hg. + +Before using hg over ssh, make sure the remote host fingerprint already exists +in ~/.ssh/known_hosts, and the remote host has this host's public key. + +.. code-block:: yaml + + https://bitbucket.org/example_user/example_repo: + hg.latest: + - rev: tip + - target: /tmp/example_repo +''' +import logging +import os +import shutil + +from salt.states.git import _fail, _neutral_test + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only load if hg is available + ''' + return 'hg' if __salt__['cmd.has_exec']('hg') else False + + +def latest(name, + rev=None, + target=None, + runas=None, + force=False, + ): + ''' + Make sure the repository is cloned to to given directory and is up to date + + name + Address of the remote repository as passed to "hg clone" + rev + The remote branch, tag, or revision hash to clone/pull + target + Name of the target directory where repository is about to be cloned + runas + Name of the user performing repository management operations + force + Force hg to clone into pre-existing directories (deletes contents) + ''' + ret = {'name': name, 'result': True, 'comment': '', 'changes': {}} + if not target: + return _fail(ret, '"target option is required') + + is_repository = ( + os.path.isdir(target) and + os.path.isdir('{0}/.hg'.format(target))) + + if is_repository: + ret = _update_repo(ret, target, runas, rev) + else: + if os.path.isdir(target): + fail = _handle_existing(ret, target, force) + if fail is not None: + return fail + else: + log.debug( + 'target {0} is not found, "hg clone" is required'.format( + target)) + if __opts__['test']: + return _neutral_test( + ret, + 'Repository {0} is about to be cloned to {1}'.format( + name, target)) + _clone_repo(ret, target, name, runas, rev) + return ret + + +def _update_repo(ret, target, runas, rev): + log.debug( + 'target {0} is found, ' + '"hg pull && hg up is probably required"'.format(target) + ) + + current_rev = __salt__['hg.revision'](target, user=runas) + if not current_rev: + return _fail( + ret, + 'Seems that {0} is not a valid hg repo'.format(target)) + + if __opts__['test']: + test_result = ( + 'Repository {0} update is probably required (current ' + 'revision is {1})').format(target, current_rev) + return _neutral_test( + ret, + test_result) + + pull_out = __salt__['hg.pull'](target, user=runas) + + if rev: + __salt__['hg.update'](target, rev, user=runas) + else: + __salt__['hg.update'](target, 'tip', user=runas) + + new_rev = __salt__['hg.revision'](cwd=target, user=runas) + + if current_rev != new_rev: + revision_text = '{0} => {1}'.format(current_rev, new_rev) + log.info( + 'Repository {0} updated: {1}'.format( + target, revision_text) + ) + ret['comment'] = 'Repository {0} updated.'.format(target) + ret['changes']['revision'] = revision_text + elif 'error:' in pull_out: + return _fail( + ret, + 'An error was thrown by hg:\n{0}'.format(pull_out) + ) + return ret + + +def _handle_existing(ret, target, force): + is_empty = os.listdir(target) + if is_empty: + log.debug( + 'target {0} found, but directory is empty, automatically ' + 'deleting'.format(target)) + shutil.rmtree(target) + elif force: + log.debug( + 'target {0} found and is not empty. Since force option is' + ' in use, deleting anyway.'.format(target)) + shutil.rmtree(target) + else: + return _fail(ret, 'Directory exists, and is not empty') + + +def _clone_repo(ret, target, name, runas, rev): + result = __salt__['hg.clone'](target, name, user=runas) + + if not os.path.isdir(target): + return _fail(ret, result) + + if rev: + __salt__['hg.update'](target, rev, user=runas) + + new_rev = __salt__['hg.revision'](cwd=target, user=runas) + message = 'Repository {0} cloned to {1}'.format(name, target) + log.info(message) + ret['comment'] = message + + ret['changes']['new'] = name + ret['changes']['revision'] = new_rev + + return ret diff --git a/salt/states/module.py b/salt/states/module.py index 489d46aaf5..e8fb6bef4f 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -18,10 +18,10 @@ def wait(name, **kwargs): ''' Run a single module function only if the watch statement calls it - name + ``name`` The module function to execute - **kwargs + ``**kwargs`` Pass any arguments needed to execute the function ''' return {'name': name, @@ -34,10 +34,10 @@ def run(name, **kwargs): ''' Run a single module function - name + ``name`` The module function to execute - **kwargs + ``**kwargs`` Pass any arguments needed to execute the function ''' ret = {'name': name, @@ -112,10 +112,11 @@ def run(name, **kwargs): except Exception: ret['comment'] = 'Module function {0} threw an exception'.format(name) ret['result'] = False + else: + ret['changes']['ret'] = mret ret['comment'] = 'Module function {0} executed'.format(name) ret['result'] = True - ret['changes']['ret'] = mret return ret mod_watch = run diff --git a/salt/states/mongodb_database.py b/salt/states/mongodb_database.py new file mode 100644 index 0000000000..5c302361b6 --- /dev/null +++ b/salt/states/mongodb_database.py @@ -0,0 +1,51 @@ +''' +Management of Mongodb databases + +Only deletion is supported, creation doesnt make sense +and can be done using mongodb_user.present +''' +def absent(name, + user=None, + password=None, + host=None, + port=None): + ''' + Ensure that the named database is absent + + name + The name of the database to remove + + user + The user to connect as (must be able to create the user) + + password + The password of the user + + host + The host to connect to + + port + The port to connect to + + ''' + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': ''} + + #check if database exists and remove it + if __salt__['mongodb.db_exists'](name, user, password, host, port): + if __opts__['test']: + ret['result'] = None + ret['comment'] = ('Database {0} is present and needs to be removed' + ).format(name) + return ret + if __salt__['mongodb.db_remove'](name, user, password, host, port): + ret['comment'] = 'Database {0} has been removed'.format(name) + ret['changes'][name] = 'Absent' + return ret + + # fallback + ret['comment'] = ('User {0} is not present, so it cannot be removed' + ).format(name) + return ret diff --git a/salt/states/mongodb_user.py b/salt/states/mongodb_user.py new file mode 100644 index 0000000000..7f1fcc74ae --- /dev/null +++ b/salt/states/mongodb_user.py @@ -0,0 +1,110 @@ +''' +Management of Mongodb users +=========================== +''' + +def present(name, + passwd, + database="admin", + user=None, + password=None, + host=None, + port=None): + ''' + Ensure that the user is present with the specified properties + + name + The name of the user to manage + + passwd + The password of the user + + user + The user to connect as (must be able to create the user) + + password + The password of the user + + host + The host to connect to + + port + The port to connect to + + database + The database to create the user in (if the db doesn't exist, it will be created) + + ''' + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': 'User {0} is already present'.format(name)} + # check if user exists + if __salt__['mongodb.user_exists'](name, user, password, host, port): + return ret + + if __opts__['test']: + ret['result'] = None + ret['comment'] = ('User {0} is not present and needs to be created' + ).format(name) + return ret + # The user is not present, make it! + if __salt__['mongodb.user_create'](name, passwd, user, password, host, port, database=database): + ret['comment'] = 'User {0} has been created'.format(name) + ret['changes'][name] = 'Present' + else: + ret['comment'] = 'Failed to create database {0}'.format(name) + ret['result'] = False + + return ret + + +def absent(name, + user=None, + password=None, + host=None, + port=None, + database="admin"): + ''' + Ensure that the named user is absent + + name + The name of the user to remove + + user + The user to connect as (must be able to create the user) + + password + The password of the user + + host + The host to connect to + + port + The port to connect to + + database + The database to create the user in (if the db doesn't exist, it will be created) + + ''' + ret = {'name': name, + 'changes': {}, + 'result': True, + 'comment': ''} + + #check if user exists and remove it + if __salt__['mongodb.user_exists'](name, user, password, host, port, database=database): + if __opts__['test']: + ret['result'] = None + ret['comment'] = ('User {0} is present and needs to be removed' + ).format(name) + return ret + if __salt__['mongodb.user_remove'](name, user, password, host, port, database=database): + ret['comment'] = 'User {0} has been removed'.format(name) + ret['changes'][name] = 'Absent' + return ret + + # fallback + ret['comment'] = ('User {0} is not present, so it cannot be removed' + ).format(name) + return ret diff --git a/salt/states/network.py b/salt/states/network.py index 83d1002520..9286d115bb 100644 --- a/salt/states/network.py +++ b/salt/states/network.py @@ -41,6 +41,13 @@ supported. This module will therefore only work on RH/CentOS/Fedora. - type: slave - master: bond0 + eth4: + network.managed: + - enabled: True + - type: eth + - proto: dhcp + - bridge: br0 + bond0: network.managed: - type: bond @@ -114,6 +121,18 @@ supported. This module will therefore only work on RH/CentOS/Fedora. - network: bond0 - require: - network: bond0 + br0: + network.managed: + - enabled: True + - type: bridge + - proto: dhcp + - bridge: br0 + - delay: 0 + - bypassfirewall: True + - use: + - network: eth4 + - require: + - network: eth4 ''' import difflib @@ -146,6 +165,7 @@ def managed(name, type, enabled=True, **kwargs): 'result': True, 'comment': 'Interface {0} is up to date.'.format(name) } + kwargs['test'] = __opts__['test'] # Build interface try: @@ -153,23 +173,21 @@ def managed(name, type, enabled=True, **kwargs): new = __salt__['ip.build_interface'](name, type, enabled, kwargs) if __opts__['test']: if old == new: - return ret + pass if not old and new: ret['result'] = None - ret['comment'] = 'Interface {0} is set to be added.' - ret['comment'] = ret['comment'].format(name) - return ret + ret['comment'] = 'Interface {0} is set to be added.'.format(name) elif old != new: + diff = difflib.unified_diff(old, new) ret['result'] = None - ret['comment'] = 'Interface {0} is set to be updated.' - ret['comment'] = ret['comment'].format( - name) - return ret - if not old and new: - ret['changes']['interface'] = 'Added network interface.' - elif old != new: - diff = difflib.unified_diff(old, new) - ret['changes']['interface'] = ''.join(diff) + ret['comment'] = 'Interface {0} is set to be updated.'.format(name) + ret['changes']['interface'] = ''.join(diff) + else: + if not old and new: + ret['changes']['interface'] = 'Added network interface.' + elif old != new: + diff = difflib.unified_diff(old, new) + ret['changes']['interface'] = ''.join(diff) except AttributeError as error: ret['result'] = False ret['comment'] = error.message @@ -180,17 +198,32 @@ def managed(name, type, enabled=True, **kwargs): try: old = __salt__['ip.get_bond'](name) new = __salt__['ip.build_bond'](name, kwargs) - if not old and new: - ret['changes']['bond'] = 'Added bond.' - elif old != new: - diff = difflib.unified_diff(old, new) - ret['changes']['bond'] = ''.join(diff) + if __opts__['test']: + if old == new: + pass + if not old and new: + ret['result'] = None + ret['comment'] = 'Bond interface {0} is set to be added.'.format(name) + elif old != new: + diff = difflib.unified_diff(old, new) + ret['result'] = None + ret['comment'] = 'Bond interface {0} is set to be updated.'.format(name) + ret['changes']['bond'] = ''.join(diff) + else: + if not old and new: + ret['changes']['bond'] = 'Added bond {0}.'.format(name) + elif old != new: + diff = difflib.unified_diff(old, new) + ret['changes']['bond'] = ''.join(diff) except AttributeError as error: #TODO Add a way of reversing the interface changes. ret['result'] = False ret['comment'] = error.message return ret + if __opts__['test']: + return ret + #Bring up/shutdown interface try: if enabled: @@ -224,6 +257,7 @@ def system(name, **kwargs): 'comment': 'Global network settings are up to date.' } apply_net_settings = False + kwargs['test'] = __opts__['test'] # Build global network settings try: old = __salt__['ip.get_network_settings']() @@ -236,14 +270,14 @@ def system(name, **kwargs): ret['comment'] = 'Global network settings are set to be added.' return ret elif old != new: + diff = difflib.unified_diff(old, new) ret['result'] = None - ret['comment'] = \ - 'Global network settings are set to be updated.' + ret['comment'] = 'Global network settings are set to be updated.' + ret['changes']['network_settings'] = ''.join(diff) return ret if not old and new: apply_net_settings = True - ret['changes']['network_settings'] = \ - 'Added global network settings.' + ret['changes']['network_settings'] = 'Added global network settings.' elif old != new: diff = difflib.unified_diff(old, new) apply_net_settings = True diff --git a/salt/states/pecl.py b/salt/states/pecl.py new file mode 100644 index 0000000000..36317bc067 --- /dev/null +++ b/salt/states/pecl.py @@ -0,0 +1,64 @@ +''' +Installation of PHP pecl extensions. +============================================== + +A state module to manage php pecl extensions. + +.. code-block:: yaml + + mongo: + pecl.installed +''' + + +def installed(name): + ''' + Make sure that a pecl extension is installed. + + name + The pecl extension name to install + ''' + ret = {'name': name, 'result': None, 'comment': '', 'changes': {}} + if name in __salt__['pecl.list'](): + ret['result'] = True + ret['comment'] = 'Pecl is already installed.' + return ret + + if __opts__['test']: + ret['comment'] = 'The pecl {0} would have been installed'.format(name) + return ret + if __salt__['pecl.install'](name): + ret['result'] = True + ret['changes'][name] = 'Installed' + ret['comment'] = 'Pecl was successfully installed' + else: + ret['result'] = False + ret['comment'] = 'Could not install pecl.' + + return ret + + +def removed(name): + ''' + Make sure that a pecl extension is not installed. + + name + The pecl exntension name to uninstall + ''' + ret = {'name': name, 'result': None, 'comment': '', 'changes': {}} + if name not in __salt__['pecl.list'](): + ret['result'] = True + ret['comment'] = 'Pecl is not installed.' + return ret + + if __opts__['test']: + ret['comment'] = 'The pecl {0} would have been removed'.format(name) + return ret + if __salt__['pecl.uninstall'](name): + ret['result'] = True + ret['changes'][name] = 'Removed' + ret['comment'] = 'Pecl was successfully removed.' + else: + ret['result'] = False + ret['comment'] = 'Could not remove pecl.' + return ret diff --git a/salt/states/pip.py b/salt/states/pip.py index 12351bedbd..76fc57ce7a 100644 --- a/salt/states/pip.py +++ b/salt/states/pip.py @@ -11,6 +11,9 @@ A state module to manage system installed python packages - version: 3.0.1 ''' +# Import Salt libs +from salt.exceptions import CommandExecutionError, CommandNotFoundError + def installed(name, pip_bin=None, @@ -59,7 +62,14 @@ def installed(name, bin_env = env ret = {'name': name, 'result': None, 'comment': '', 'changes': {}} - if name in __salt__['pip.list'](name, bin_env, runas=user, cwd=cwd): + try: + pip_list = __salt__['pip.list'](name, bin_env, runas=user, cwd=cwd) + except (CommandNotFoundError, CommandExecutionError), err: + ret['result'] = False + ret['comment'] = 'Error installing \'{0}\': {1}'.format(name, err) + return ret + + if name in pip_list: ret['result'] = True ret['comment'] = 'Package already installed' return ret @@ -70,38 +80,56 @@ def installed(name, name) return ret - if __salt__['pip.install'](pkgs=name, - requirements=requirements, - bin_env=bin_env, - log=log, - proxy=proxy, - timeout=timeout, - editable=editable, - find_links=find_links, - index_url=index_url, - extra_index_url=extra_index_url, - no_index=no_index, - mirrors=mirrors, - build=build, - target=target, - download=download, - download_cache=download_cache, - source=source, - upgrade=upgrade, - force_reinstall=force_reinstall, - ignore_installed=ignore_installed, - no_deps=no_deps, - no_install=no_install, - no_download=no_download, - install_options=install_options, - runas=user, - cwd=cwd): + pip_install_call = __salt__['pip.install']( + pkgs=name, + requirements=requirements, + bin_env=bin_env, + log=log, + proxy=proxy, + timeout=timeout, + editable=editable, + find_links=find_links, + index_url=index_url, + extra_index_url=extra_index_url, + no_index=no_index, + mirrors=mirrors, + build=build, + target=target, + download=download, + download_cache=download_cache, + source=source, + upgrade=upgrade, + force_reinstall=force_reinstall, + ignore_installed=ignore_installed, + no_deps=no_deps, + no_install=no_install, + no_download=no_download, + install_options=install_options, + runas=user, + cwd=cwd + ) + + if pip_install_call and pip_install_call['retcode']==0: + ret['result'] = True + pkg_list = __salt__['pip.list'](name, bin_env, runas=user, cwd=cwd) + if not pkg_list: + ret['comment'] = ( + 'There was no error installing package \'{0}\' although ' + 'it does not show when calling \'pip.freeze\'.'.format(name) + ) + ret['changes']["{0}==???".format(name)] = 'Installed' + return ret + version = list(pkg_list.values())[0] pkg_name = next(iter(pkg_list)) - ret['result'] = True ret['changes']["{0}=={1}".format(pkg_name, version)] = 'Installed' ret['comment'] = 'Package was successfully installed' + elif pip_install_call: + ret['result'] = False + ret['comment'] = 'Failed to install package {0}. Error: {1}'.format( + name, pip_install_call['stderr'] + ) else: ret['result'] = False ret['comment'] = 'Could not install package' @@ -128,10 +156,17 @@ def removed(name, """ ret = {'name': name, 'result': None, 'comment': '', 'changes': {}} - if name not in __salt__["pip.list"](packages=name, bin_env=bin_env, - runas=user, cwd=cwd): + + try: + pip_list = __salt__["pip.list"](bin_env=bin_env, runas=user, cwd=cwd) + except (CommandExecutionError, CommandNotFoundError), err: + ret['result'] = False + ret['comment'] = 'Error uninstalling \'{0}\': {1}'.format(name, err) + return ret + + if name not in pip_list: ret["result"] = True - ret["comment"] = "Pacakge is not installed." + ret["comment"] = "Package is not installed." return ret if __opts__['test']: @@ -139,7 +174,7 @@ def removed(name, ret['comment'] = 'Package {0} is set to be removed'.format(name) return ret - if __salt__["pip.uninstall"](packages=name, + if __salt__["pip.uninstall"](pkgs=name, requirements=requirements, bin_env=bin_env, log=log, diff --git a/salt/states/postgres_user.py b/salt/states/postgres_user.py index 62de6d0708..92761c32b3 100644 --- a/salt/states/postgres_user.py +++ b/salt/states/postgres_user.py @@ -55,9 +55,13 @@ def present(name, ret['result'] = None ret['comment'] = 'User {0} is set to be created'.format(name) return ret - if __salt__['postgres.user_create'](username=name, createdb=createdb, - createuser=createuser, encrypted=encrypted, - password=password, runas=runas): + if __salt__['postgres.user_create'](username=name, + createdb=createdb, + createuser=createuser, + encrypted=encrypted, + superuser=superuser, + password=password, + runas=runas): ret['comment'] = 'The user {0} has been created'.format(name) ret['changes'][name] = 'Present' else: diff --git a/salt/states/selinux.py b/salt/states/selinux.py index 4b894c4ba9..d84b7c2b63 100644 --- a/salt/states/selinux.py +++ b/salt/states/selinux.py @@ -11,7 +11,7 @@ booleans can be set. selinux.mode samba_create_home_dirs: - selinx.boolean: + selinux.boolean: - value: True - persist: True @@ -63,7 +63,7 @@ def mode(name): 'result': False, 'comment': '', 'changes': {}} - tmode = _refine_mode(mode) + tmode = _refine_mode(name) if tmode == 'unknown': ret['comment'] = '{0} is not an accepted mode'.format(name) return ret diff --git a/salt/states/service.py b/salt/states/service.py index db11e83ec2..3e31ae0b77 100644 --- a/salt/states/service.py +++ b/salt/states/service.py @@ -20,25 +20,6 @@ def __virtual__(): return 'service' -def _get_stat(name, sig): - ''' - Return the status of a service based on signature and status, if the - signature is used then the status option will be ignored - ''' - stat = False - if sig: - if sig == 'detect': - cmd = "{0[ps]} | grep {1} | grep -v grep | awk '{{print $2}}'" - cmd = cmd.format(__grains__, name) - else: - cmd = "{0[ps]} | grep {1} | grep -v grep | awk '{{print $2}}'" - cmd = cmd.format(__grains__, sig) - stat = bool(__salt__['cmd.run'](cmd)) - else: - stat = __salt__['service.status'](name) - return stat - - def _enable(name, started): ''' Enable the service @@ -67,7 +48,6 @@ def _enable(name, started): if __salt__['service.enabled'](name): # Service is enabled if started is True: - ret['changes'][name] = True ret['comment'] = ('Service {0} is already enabled,' ' and is running').format(name) return ret @@ -227,7 +207,7 @@ def running(name, enable=None, sig=None): 'result': True, 'comment': ''} # See if the service is already running - if _get_stat(name, sig): + if __salt__['service.status'](name, sig): ret['comment'] = 'The service {0} is already running'.format(name) if enable is True: return _enable(name, None) @@ -293,7 +273,7 @@ def dead(name, enable=None, sig=None): 'changes': {}, 'result': True, 'comment': ''} - if not _get_stat(name, sig): + if not __salt__['service.status'](name, sig): ret['comment'] = 'The service {0} is already dead'.format(name) if enable is True: return _enable(name, None) @@ -301,6 +281,17 @@ def dead(name, enable=None, sig=None): return _disable(name, None) else: return ret + + # Check if the service is available + if 'service.get_all' in __salt__: + # get_all is available, we can reliable check for the service + services = __salt__['service.get_all']() + if not name in services: + ret['result'] = False + ret['comment'] = 'The named service {0} is not available'.format( + name) + return ret + if __opts__['test']: ret['result'] = None ret['comment'] = 'Service {0} is set to be killed'.format(name) @@ -352,7 +343,7 @@ def disabled(name): return _disable(name, None) -def mod_watch(name, sig=None, reload=False): +def mod_watch(name, sig=None, reload=False, full_restart=False): ''' The service watcher, called to invoke the watch command. @@ -364,15 +355,18 @@ def mod_watch(name, sig=None, reload=False): ''' if __salt__['service.status'](name, sig): if 'service.reload' in __salt__ and reload: - changes = {name: __salt__['service.reload'](name)} + restart_func = __salt__['service.reload'] + elif 'service.full_restart' in __salt__ and full_restart: + restart_func = __salt__['service.full_restart'] else: - changes = {name: __salt__['service.restart'](name)} - return {'name': name, - 'changes': changes, - 'result': True, - 'comment': 'Service restarted'} + restart_func = __salt__['service.restart'] + else: + restart_func = __salt__['service.start'] + result = restart_func(name) return {'name': name, - 'changes': {}, - 'result': True, - 'comment': 'Service {0} started'.format(name)} + 'changes': {name: result}, + 'result': result, + 'comment': 'Service restarted' if result else \ + 'Failed to restart the service' + } diff --git a/salt/states/supervisord.py b/salt/states/supervisord.py new file mode 100644 index 0000000000..fa0396e571 --- /dev/null +++ b/salt/states/supervisord.py @@ -0,0 +1,102 @@ +''' +Interaction with the Supervisor daemon. +======================================= + +.. code-block:: yaml + + wsgi_server: + supervisord: + - running + - restart: False + - require: + - pkg: supervisor +''' +import logging + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Check for supervisorctl script + ''' + if __salt__['cmd.has_exec']('supervisorctl'): + return 'supervisord' + return False + + +def _check_error(result, success_message): + ret = {} + + if 'ERROR' in result: + ret['comment'] = result + ret['result'] = False + else: + ret['comment'] = success_message + + return ret + +def running(name, + restart=False, + runas=None, + ): + ''' + Ensure the named service is running. + + name + Service name as defined in the supervisor configuration file + restart + Whether to force a restart e.g. when updating a service + runas + Name of the user to run the supervisorctl command + ''' + ret = {'name': name, 'result': True, 'comment': '', 'changes': ''} + + if __opts__['test']: + ret['result'] = None + _msg = 'restarted' if restart else 'started' + ret['comment'] = ( + 'Service {0} is set to be {1}'.format( + name, _msg)) + elif restart: + comment = 'Restarting service: {0}'.format(name) + log.debug(comment) + result = __salt__['supervisord.restart'](name, user=runas) + + ret.update(_check_error(result, comment)) + + else: + comment = 'Starting service: {0}'.format(name) + log.debug(comment) + result = __salt__['supervisord.start'](name, user=runas) + + ret.update(_check_error(result, comment)) + + log.debug(unicode(result)) + return ret + + +def dead(name, + runas=None): + ''' + Ensure the named service is dead (not running). + + name + Service name as defined in the supervisor configuration file + runas + Name of the user to run the supervisorctl command + ''' + ret = {'name': name, 'result': True, 'comment': '', 'changes': ''} + + if __opts__['test']: + ret['comment'] = ( + 'Service {0} is set to be stopped'.format(name)) + else: + comment = 'Stopping service: {0}'.format(name) + log.debug(comment) + result = __salt__['supervisord.stop'](name, user=runas) + + ret.update(_check_error(result, comment)) + + log.debug(unicode(result)) + return ret diff --git a/salt/states/sysctl.py b/salt/states/sysctl.py index 8a01d57cc9..aa96d9de0e 100644 --- a/salt/states/sysctl.py +++ b/salt/states/sysctl.py @@ -32,7 +32,6 @@ def present(name, value, config='/etc/sysctl.conf'): current = __salt__['sysctl.show']() if __opts__['test']: - print current if name in current: if current[name] != str(value): ret['result'] = None diff --git a/salt/states/user.py b/salt/states/user.py index ef27816e28..b6dc24d8d9 100644 --- a/salt/states/user.py +++ b/salt/states/user.py @@ -101,6 +101,7 @@ def present( name, uid=None, gid=None, + gid_from_name=False, groups=None, home=True, password=None, @@ -126,6 +127,9 @@ def present( gid The default group id + + gid_from_name + If True, the default group id will be set to the id of the group with the same name as the user. groups A list of groups to assign the user to, pass a list object @@ -178,6 +182,8 @@ def present( 'result': True, 'comment': 'User {0} is present and up to date'.format(name)} + if gid_from_name: + gid = __salt__['file.group_to_gid'](name) changes = _changes( name, uid, diff --git a/salt/states/virtualenv.py b/salt/states/virtualenv.py index 5733e5603a..779d6ec5ef 100644 --- a/salt/states/virtualenv.py +++ b/salt/states/virtualenv.py @@ -43,7 +43,7 @@ def managed(name, - no_site_packages: True - requirements: salt://REQUIREMENTS.txt ''' - ret = {'name': name, 'result': None, 'comment': '', 'changes': {}} + ret = {'name': name, 'result': True, 'comment': '', 'changes': {}} if not 'virtualenv.create' in __salt__: ret['result'] = False @@ -79,7 +79,7 @@ def managed(name, return ret if not venv_exists or (venv_exists and clear): - __salt__['virtualenv.create'](name, + _ret = __salt__['virtualenv.create'](name, venv_bin=venv_bin, no_site_packages=no_site_packages, system_site_packages=system_site_packages, @@ -91,6 +91,7 @@ def managed(name, prompt=prompt, runas=runas) + ret['result'] = _ret['retcode']==0 ret['changes']['new'] = __salt__['cmd.run_stderr']( '{0} -V'.format(venv_py)).strip('\n') @@ -104,12 +105,12 @@ def managed(name, # Populate the venv via a requirements file if requirements: - if requirements.startswith('salt://'): - requirements = __salt__['cp.cache_file'](requirements, __env__) before = set(__salt__['pip.freeze'](bin_env=name)) - __salt__['pip.install']( + _ret = __salt__['pip.install']( requirements=requirements, bin_env=name, runas=runas, cwd=cwd ) + ret['result'] &= _ret['retcode']==0 + after = set(__salt__['pip.freeze'](bin_env=name)) new = list(after - before) @@ -119,7 +120,6 @@ def managed(name, ret['changes']['packages'] = { 'new': new if new else '', 'old': old if old else ''} - ret['result'] = True return ret manage = managed diff --git a/salt/template.py b/salt/template.py index dde31ff8c4..d9697115b7 100644 --- a/salt/template.py +++ b/salt/template.py @@ -48,9 +48,7 @@ def compile_template_str(template, renderers, default): os.close(fd_) with open(fn_, 'w+') as f: f.write(template) - high = renderers[template_shebang(fn_, renderers, default)](fn_) - os.remove(fn_) - return high + return compile_template(fn_, renderers, default) def template_shebang(template, renderers, default): diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index f68096a38d..e80bac3486 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -5,6 +5,7 @@ from __future__ import absolute_import # Import Python libs import os +import re import imp import random import sys @@ -13,8 +14,10 @@ import logging import hashlib import datetime import tempfile +import shlex import shutil import time +import platform from calendar import month_abbr as months # Import Salt libs @@ -64,7 +67,7 @@ def is_empty(filename): try: return os.stat(filename).st_size == 0 except OSError: - # Non-existant file or permission denied to the parent dir + # Non-existent file or permission denied to the parent dir return False @@ -111,49 +114,6 @@ def daemonize(): ''' Daemonize a process ''' - if 'os' in os.environ: - if os.environ['os'].startswith('Windows'): - import ctypes - if ctypes.windll.shell32.IsUserAnAdmin() == 0: - import win32api - executablepath = sys.executable - pypath = executablepath.split('\\') - win32api.ShellExecute( - 0, - 'runas', - executablepath, - os.path.join( - pypath[0], - os.sep, - pypath[1], - 'Lib\\site-packages\\salt\\utils\\saltminionservice.py' - ), - os.path.join(pypath[0], os.sep, pypath[1]), - 0 - ) - sys.exit(0) - else: - from . import saltminionservice - import win32serviceutil - import win32service - import winerror - servicename = 'salt-minion' - try: - status = win32serviceutil.QueryServiceStatus(servicename) - except win32service.error as details: - if details[0] == winerror.ERROR_SERVICE_DOES_NOT_EXIST: - saltminionservice.instart( - saltminionservice.MinionService, - servicename, - 'Salt Minion' - ) - sys.exit(0) - if status[1] == win32service.SERVICE_RUNNING: - win32serviceutil.StopServiceWithDeps(servicename) - win32serviceutil.StartService(servicename) - else: - win32serviceutil.StartService(servicename) - sys.exit(0) try: pid = os.fork() if pid > 0: @@ -182,7 +142,7 @@ def daemonize(): # A normal daemonization redirects the process output to /dev/null. # Unfortunately when a python multiprocess is called the output is # not cleanly redirected and the parent process dies when the - # multiprocessing process attemps to access stdout or err. + # multiprocessing process attempts to access stdout or err. #dev_null = open('/dev/null', 'rw') #os.dup2(dev_null.fileno(), sys.stdin.fileno()) #os.dup2(dev_null.fileno(), sys.stdout.fileno()) @@ -198,6 +158,8 @@ def daemonize_if(opts, **kwargs): return if not opts['multiprocessing']: return + if sys.platform.startswith('win'): + return # Daemonizing breaks the proc dir, so the proc needs to be rewritten data = {} for key, val in kwargs.items(): @@ -245,7 +207,11 @@ def which(exe=None): (path, name) = os.path.split(exe) if os.access(exe, os.X_OK): return exe - for path in os.environ.get('PATH').split(os.pathsep): + + # default path based on busybox's default + default_path = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin" + + for path in os.environ.get('PATH', default_path).split(os.pathsep): full_path = os.path.join(path, exe) if os.access(full_path, os.X_OK): return full_path @@ -414,13 +380,15 @@ def jid_dir(jid, cachedir, sum_type): def check_or_die(command): ''' - Simple convienence function for modules to use - for gracefully blowing up if a required tool is - not available in the system path. + Simple convenience function for modules to use for gracefully blowing up + if a required tool is not available in the system path. - Lazily import salt.modules.cmdmod to avoid any - sort of circular dependencies. + Lazily import `salt.modules.cmdmod` to avoid any sort of circular + dependencies. ''' + if command is None: + raise CommandNotFoundError("'None' is not a valid command.") + import salt.modules.cmdmod __salt__ = {'cmd.has_exec': salt.modules.cmdmod.has_exec} @@ -446,22 +414,128 @@ def copyfile(source, dest, backup_mode='', cachedir=''): fd_, tgt = tempfile.mkstemp(prefix=bname, dir=dname) os.close(fd_) shutil.copyfile(source, tgt) + mask = os.umask(0) + os.umask(mask) + os.chmod(tgt, 0666 - mask) bkroot = '' if cachedir: bkroot = os.path.join(cachedir, 'file_backup') if backup_mode == 'minion' or backup_mode == 'both' and bkroot: - msecs = str(int(time.time() * 1000000))[-6:] - stamp = time.asctime().replace(' ', '_') - stamp = '{0}{1}_{2}'.format(stamp[:-4], msecs, stamp[-4:]) - bkpath = os.path.join( - bkroot, - dname[1:], - '{0}_{1}'.format(bname, stamp) - ) - if not os.path.isdir(os.path.dirname(bkpath)): - os.makedirs(os.path.dirname(bkpath)) - shutil.copyfile(source, bkpath) + if os.path.exists(dest): + fstat = os.stat(dest) + msecs = str(int(time.time() * 1000000))[-6:] + stamp = time.asctime().replace(' ', '_') + stamp = '{0}{1}_{2}'.format(stamp[:-4], msecs, stamp[-4:]) + bkpath = os.path.join( + bkroot, + dname[1:], + '{0}_{1}'.format(bname, stamp) + ) + if not os.path.isdir(os.path.dirname(bkpath)): + os.makedirs(os.path.dirname(bkpath)) + shutil.copyfile(dest, bkpath) + os.chown(bkpath, fstat.st_uid, fstat.st_gid) if backup_mode == 'master' or backup_mode == 'both' and bkroot: # TODO, backup to master pass - shutil.move(tgt, dest) + try: + shutil.move(tgt, dest) + except Exception: + pass + if os.path.isfile(tgt): + # The temp file failed to move + try: + os.remove(tgt) + except Exception: + pass + + +def path_join(*parts): + ''' + This functions tries to solve some issues when joining multiple absolute + paths on both *nix and windows platforms. + + See tests/unit/utils/path_join_test.py for some examples on what's being + talked about here. + ''' + # Normalize path converting any os.sep as needed + parts = [os.path.normpath(p) for p in parts] + + root = parts.pop(0) + if not parts: + return root + + if platform.system().lower() == 'windows': + if len(root) == 1: + root += ':' + root = root.rstrip(os.sep) + os.sep + + return os.path.normpath(os.path.join( + root, *[p.lstrip(os.sep) for p in parts] + )) + + +def pem_finger(path, sum_type='md5'): + ''' + Pass in the location of a pem file and the type of cryptographic hash to + use. The default is md5. + ''' + if not os.path.isfile(path): + return '' + with open(path, 'rb') as fp_: + key = ''.join(fp_.readlines()[1:-1]) + pre = getattr(hashlib, sum_type)(key).hexdigest() + finger = '' + for ind in range(len(pre)): + if ind % 2: + # Is odd + finger += '{0}:'.format(pre[ind]) + else: + finger += pre[ind] + return finger.rstrip(':') + + +def build_whitepace_splited_regex(text): + ''' + Create a regular expression at runtime which should match ignoring the + addition or deletion of white space or line breaks, unless between commas + + Example:: + + >>> import re + >>> from salt.utils import * + >>> regex = build_whitepace_splited_regex( + ... """if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then""" + ... ) + + >>> regex + '(?:[\\s]+)?if(?:[\\s]+)?\\[(?:[\\s]+)?\\-z(?:[\\s]+)?\\"\\$debian' + '\\_chroot\\"(?:[\\s]+)?\\](?:[\\s]+)?\\&\\&(?:[\\s]+)?\\[(?:[\\s]+)?' + '\\-r(?:[\\s]+)?\\/etc\\/debian\\_chroot(?:[\\s]+)?\\]\\;(?:[\\s]+)?' + 'then(?:[\\s]+)?' + >>> re.search( + ... regex, + ... """if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then""" + ... ) + + <_sre.SRE_Match object at 0xb70639c0> + >>> + + ''' + + def __build_parts(text): + lexer = shlex.shlex(text) + lexer.whitespace_split = True + lexer.commenters = '' + if '"' in text: + lexer.quotes = '"' + elif '\'' in text: + lexer.quotes = '\'' + return list(lexer) + + + regex = r'' + for line in text.splitlines(): + parts = [re.escape(s) for s in __build_parts(line)] + regex += r'(?:[\s]+)?{0}(?:[\s]+)?'.format(r'(?:[\s]+)?'.join(parts)) + return regex diff --git a/salt/utils/atomicfile.py b/salt/utils/atomicfile.py index 3595a3aff0..f47d464cca 100644 --- a/salt/utils/atomicfile.py +++ b/salt/utils/atomicfile.py @@ -11,7 +11,7 @@ import random can_rename_open_file = False -if os.name == 'nt': # pragma: no cover +if os.name == 'nt': # pragma: no cover _rename = lambda src, dst: False _rename_atomic = lambda src, dst: False @@ -79,7 +79,7 @@ if os.name == 'nt': # pragma: no cover except OSError, e: if e.errno != errno.EEXIST: raise - old = "%s-%08x" % (dst, random.randint(0, sys.maxint)) + old = '{0}-{1:08x}'.format(dst, random.randint(0, sys.maxint)) os.rename(dst, old) os.rename(src, dst) try: @@ -123,7 +123,7 @@ class _AtomicWFile(object): pass def __repr__(self): - return '<%s %s%r, mode %r>' % ( + return '<{0} {1}{2}, mode {3}>'.format( self.__class__.__name__, self._f.closed and 'closed ' or '', self._filename, diff --git a/salt/utils/event.py b/salt/utils/event.py index 5ef1bf9737..dd587c5dbc 100644 --- a/salt/utils/event.py +++ b/salt/utils/event.py @@ -14,7 +14,9 @@ Manage events # # Import Python libs import os +import hashlib import errno +import logging import multiprocessing # Import Third Party libs @@ -23,35 +25,59 @@ import zmq # Import Salt libs import salt.payload +log = logging.getLogger(__name__) class SaltEvent(object): ''' The base class used to manage salt events ''' - def __init__(self, sock_dir, node): + def __init__(self, node, sock_dir=None, **kwargs): self.serial = salt.payload.Serial({'serial': 'msgpack'}) self.context = zmq.Context() self.poller = zmq.Poller() self.cpub = False self.cpush = False + self.puburi, self.pulluri = self.__load_uri(sock_dir, node, **kwargs) + + def __load_uri(self, sock_dir, node, **kwargs): + ''' + Return the string uri for the location of the pull and pub sockets to + use for firing and listening to events + ''' + id_hash = hashlib.md5(kwargs.get('id', '')).hexdigest() if node == 'master': - self.puburi = 'ipc://{0}'.format(os.path.join( + puburi = 'ipc://{0}'.format(os.path.join( sock_dir, 'master_event_pub.ipc' )) - self.pulluri = 'ipc://{0}'.format(os.path.join( + pulluri = 'ipc://{0}'.format(os.path.join( sock_dir, 'master_event_pull.ipc' )) else: - self.puburi = 'ipc://{0}'.format(os.path.join( - sock_dir, - 'minion_event_pub.ipc' - )) - self.pulluri = 'ipc://{0}'.format(os.path.join( - sock_dir, - 'minion_event_pull.ipc' - )) + if kwargs.get('ipc_mode', '') == 'tcp': + puburi = 'tcp://127.0.0.1:{0}'.format( + kwargs.get('tcp_pub_port', 4510) + ) + pulluri = 'tcp://127.0.0.1:{0}'.format( + kwargs.get('tcp_pull_port', 4511) + ) + else: + puburi = 'ipc://{0}'.format(os.path.join( + sock_dir, + 'minion_event_{0}_pub.ipc'.format(id_hash) + )) + pulluri = 'ipc://{0}'.format(os.path.join( + sock_dir, + 'minion_event_{0}_pull.ipc'.format(id_hash) + )) + log.debug( + '{0} PUB socket URI: {1}'.format(self.__class__.__name__, puburi) + ) + log.debug( + '{0} PULL socket URI: {1}'.format(self.__class__.__name__, pulluri) + ) + return puburi, pulluri def connect_pub(self): ''' @@ -107,8 +133,7 @@ class SaltEvent(object): ''' if not self.cpush: self.connect_pull() - tag += 20 * '|' - tag = tag[:20] + tag = '{0:|<20}'.format(tag) event = '{0}{1}'.format(tag, self.serial.dumps(data)) self.push.send(event) return True @@ -119,7 +144,7 @@ class MasterEvent(SaltEvent): Create a master event management object ''' def __init__(self, sock_dir): - super(MasterEvent, self).__init__(sock_dir, 'master') + super(MasterEvent, self).__init__('master', sock_dir) self.connect_pub() @@ -127,8 +152,8 @@ class MinionEvent(SaltEvent): ''' Create a master event management object ''' - def __init__(self, sock_dir): - super(MinionEvent, self).__init__(sock_dir, 'minion') + def __init__(self, **kwargs): + super(MinionEvent, self).__init__('minion', **kwargs) class EventPublisher(multiprocessing.Process): @@ -160,10 +185,13 @@ class EventPublisher(multiprocessing.Process): epub_sock.bind(epub_uri) epull_sock.bind(epull_uri) # Restrict access to the sockets + pub_mode = 448 + if self.opts.get('client_acl'): + pub_mode = 511 os.chmod( os.path.join(self.opts['sock_dir'], 'master_event_pub.ipc'), - 448 + pub_mode ) os.chmod( os.path.join(self.opts['sock_dir'], diff --git a/salt/utils/filebuffer.py b/salt/utils/filebuffer.py new file mode 100644 index 0000000000..9ce1a120b9 --- /dev/null +++ b/salt/utils/filebuffer.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +''' + salt.utils.filebuffer + ~~~~~~~~~~~~~~~~~~~~~ + + :copyright: © 2012 UfSoft.org - :email:`Pedro Algarvio (pedro@algarvio.me)` + :license: Apache 2.0, see LICENSE for more details. +''' + +from salt.exceptions import SaltException + + +class InvalidFileMode(SaltException): + ''' + An invalid file mode was used to open the file passed to the buffer + ''' + + +class BufferedReader(object): + ''' + This object allows iterating through the contents of a file keeping + X configurable bytes in memory which can be used to, for example, + do regex search/matching on more than a single line. + + :type path: str + :param path: The file path to be read + + :type max_in_mem: int + :param max_in_mem: The maximum bytes kept in memory while iterating through + the file. Default 256KB. + + :type chunk_size: int + :param chunk_size: The size of each consequent read chunk. Default 32KB. + + :type mode: str + :param mode: The mode the file should be opened. **Only read modes**. + + ''' + def __init__(self, path, max_in_mem=256*1024, chunk_size=32*1024, + mode='r'): + if 'a' in mode or 'w' in mode: + raise InvalidFileMode("Cannot open file in write or append mode") + self.__path = path + self.__file = open(self.__path, mode) + self.__max_in_mem = max_in_mem + self.__chunk_size = chunk_size + self.__buffered = None + + # Public attributes + @property + def buffered(self): + return self.__buffered + + # Support iteration + def __iter__(self): + return self + + def next(self): + if self.__buffered is None: + multiplier = self.__max_in_mem / self.__chunk_size + self.__buffered = "" + else: + multiplier = 1 + self.__buffered = self.__buffered[self.__chunk_size:] + + data = self.__file.read(self.__chunk_size * multiplier) + + if not data: + self.__file.close() + raise StopIteration + + self.__buffered += data + return self.__buffered + + # Support with statements + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + pass + + +if __name__ == '__main__': + def timeit_string(fpath, max_size, chunk_size): + + sf = BufferedReader(fpath, max_size, chunk_size) + for chunk in sf: + chunk + return + + def sizeof_fmt(num): + for x in ['bytes', 'KB', 'MB', 'GB']: + if num < 1024.0: + return '{0:3.1f}{1}'.format(num, x) + num /= 1024.0 + return '{0:3.1f}{1}'.format(num, 'TB') + + import os, timeit + fpath = os.path.normpath(os.path.join( + os.path.dirname(__file__), + "../../doc/topics/tutorials/starting_states.rst" + )) + + tpath = "/tmp/starting_states.rst" + + for fmultiplier in (1, 10, 50, 100, 800, 3200): + ffile = open(tpath, "w") + while fmultiplier > 0: + ffile.write(open(fpath).read()) + fmultiplier -= 1 + + ffile.close() + + TNUMBER = 1000 + + print('Running tests against a file with the size of {0}'.format( + sizeof_fmt(os.stat(tpath).st_size)) + ) + + for idx, multiplier in enumerate([4, 8, 16, 32, 64, 128, 256]): + chunk_size = multiplier * 1024 + max_size = chunk_size * 5 + t = timeit.Timer( + "timeit_string('{0}', {1:d}, {2:d})".format( + tpath, max_size, chunk_size + ), "from __main__ import timeit_string" + ) + print("timeit_string ({0: >7} chunks; max: {1: >7}):".format( + sizeof_fmt(chunk_size), sizeof_fmt(max_size))), + print(u"{0: >6} \u00B5sec/pass".format(u"{0:0.2f}".format( + TNUMBER * t.timeit(number=TNUMBER) / TNUMBER + ))) + + print diff --git a/salt/utils/find.py b/salt/utils/find.py index cb16542e54..c45456fb86 100644 --- a/salt/utils/find.py +++ b/salt/utils/find.py @@ -94,6 +94,7 @@ except ImportError: from salt._compat import MAX_SIZE +from salt.utils.filebuffer import BufferedReader # Set up logger log = logging.getLogger(__name__) @@ -415,9 +416,9 @@ class GrepOption(Option): def match(self, dirname, filename, fstat): if not stat.S_ISREG(fstat[stat.ST_MODE]): return None - with open(os.path.join(dirname, filename), 'rb') as f: - for line in f: - if self.re.search(line): + with BufferedReader(os.path.join(dirname, filename), mode='rb') as br: + for chunk in br: + if self.re.search(chunk): return os.path.join(dirname, filename) return None diff --git a/salt/utils/interfaces.py b/salt/utils/interfaces.py index 4a3701a6c5..8bfbb5a7db 100644 --- a/salt/utils/interfaces.py +++ b/salt/utils/interfaces.py @@ -1,3 +1,9 @@ +''' +Define some default interface functions to be imported in multiple network +interface modules. +''' + + def ipaddr(interface=None): ''' Returns the IP address for a given interface @@ -10,24 +16,21 @@ def ipaddr(interface=None): out = None if interface: - data = iflist.get(interface) + data = iflist.get(interface) or dict() if data.get('inet'): - if not out: - out = list() - out += data.get('inet') + return data.get('inet')[0]['address'] if data.get('inet6'): - if not out: - out = list() - out += data.get('inet6') + return data.get('inet6')[0]['address'] return out + out = dict() for iface, data in iflist.items(): if data.get('inet'): - if not out[iface]: out[iface] = list() - out[iface] += data.get('inet') + out[iface] = data.get('inet')[0]['address'] + continue if data.get('inet6'): - if not out[iface]: out[iface] = list() - out[iface] += data.get('inet6') + out[iface] = data.get('inet6')[0]['address'] + continue return out diff --git a/salt/utils/jinja.py b/salt/utils/jinja.py index 3655f1144d..d2ef17df73 100644 --- a/salt/utils/jinja.py +++ b/salt/utils/jinja.py @@ -3,10 +3,11 @@ Jinja loading utils to enable a more powerful backend for jinja templates ''' # Import python libs from os import path +import logging # Import third-party libs -from jinja2 import Template, BaseLoader, Environment -from jinja2.loaders import split_template_path +from jinja2 import (BaseLoader, Environment, StrictUndefined, + FileSystemLoader) from jinja2.exceptions import TemplateNotFound # Import Salt libs @@ -14,18 +15,29 @@ import salt import salt.fileclient +log = logging.getLogger(__name__) + + def get_template(filename, opts, env): loader = SaltCacheLoader(opts, env) if filename.startswith(loader.searchpath): - jinja = Environment(loader=loader) + if opts.get('allow_undefined', False): + jinja = Environment(loader=loader) + else: + jinja = Environment(loader=loader, undefined=StrictUndefined) relpath = path.relpath(filename, loader.searchpath) # the template was already fetched loader.cached.append(relpath) return jinja.get_template(relpath) else: # fallback for templates outside the state tree - with open(filename, 'r') as f: - return Template(f.read()) + loader = FileSystemLoader(path.dirname(filename)) + if opts.get('allow_undefined', False): + jinja = Environment(loader=loader) + else: + jinja = Environment(loader=loader, undefined=StrictUndefined) + relpath = path.relpath(filename, path.dirname(filename)) + return jinja.get_template(relpath) class SaltCacheLoader(BaseLoader): @@ -41,6 +53,7 @@ class SaltCacheLoader(BaseLoader): self.env = env self.encoding = encoding self.searchpath = path.join(opts['cachedir'], 'files', env) + log.debug('Jinja search path: \'{0}\''.format(self.searchpath)) self._file_client = None self.cached = [] @@ -69,7 +82,12 @@ class SaltCacheLoader(BaseLoader): def get_source(self, environment, template): # checks for relative '..' paths - template = path.join(*split_template_path(template)) + if '..' in template: + log.warning( + 'Discarded template path \'{0}\', relative paths are ' + 'prohibited'.format(template) + ) + raise TemplateNotFound(template) self.check_cache(template) filepath = path.join(self.searchpath, template) with open(filepath, 'rb') as f: diff --git a/salt/utils/parser.py b/salt/utils/parser.py deleted file mode 100644 index 7b994cd609..0000000000 --- a/salt/utils/parser.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -""" - salt.utils.parser - ~~~~~~~~~~~~~~~~~ - - :copyright: © 2012 UfSoft.org - :email:`Pedro Algarvio (pedro@algarvio.me)` - :license: Apache 2.0, see LICENSE for more details. -""" - -import sys -import optparse -from salt import version - - -class OptionParser(optparse.OptionParser): - - def __init__(self, *args, **kwargs): - kwargs.setdefault("version", version.__version__) - kwargs.setdefault('usage', '%prog') - optparse.OptionParser.__init__(self, *args, **kwargs) - - def parse_args(self, args=None, values=None): - options, args = optparse.OptionParser.parse_args(self, args, values) - if options.versions_report: - self.print_versions_report() - return options, args - - def _add_version_option(self): - optparse.OptionParser._add_version_option(self) - self.add_option( - '--versions-report', action='store_true', - help="show program's dependencies version number and exit" - ) - - def print_versions_report(self, file=sys.stdout): - print >> file, '\n'.join(version.versions_report()) - self.exit() \ No newline at end of file diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py new file mode 100644 index 0000000000..17e6bfe6c0 --- /dev/null +++ b/salt/utils/parsers.py @@ -0,0 +1,1038 @@ +# -*- coding: utf-8 -*- +''' + salt.utils.parsers + ~~~~~~~~~~~~~~~~~~ + + :copyright: © 2012 UfSoft.org - :email:`Pedro Algarvio (pedro@algarvio.me)` + :license: Apache 2.0, see LICENSE for more details. +''' + +import os +import sys +import logging +import optparse +from functools import partial +from salt import config, log, version + + +def _sorted(mixins_or_funcs): + return sorted( + mixins_or_funcs, key=lambda mf: getattr(mf, '_mixin_prio_', 1000) + ) + + +class MixInMeta(type): + # This attribute here won't actually do anything. But, if you need to + # specify an order or a dependency within the mix-ins, please define the + # attribute on your own MixIn + _mixin_prio_ = 0 + + def __new__(cls, name, bases, attrs): + instance = super(MixInMeta, cls).__new__(cls, name, bases, attrs) + if not hasattr(instance, '_mixin_setup'): + raise RuntimeError( + 'Don\'t subclass {0} in {1} if you\'re not going to use it as a ' + 'salt parser mix-in.'.format(cls.__name__, name) + ) + return instance + + +class OptionParserMeta(MixInMeta): + def __new__(cls, name, bases, attrs): + instance = super(OptionParserMeta, cls).__new__(cls, name, bases, attrs) + if not hasattr(instance, '_mixin_setup_funcs'): + instance._mixin_setup_funcs = [] + if not hasattr(instance, '_mixin_process_funcs'): + instance._mixin_process_funcs = [] + if not hasattr(instance, '_mixin_after_parsed_funcs'): + instance._mixin_after_parsed_funcs = [] + + for base in _sorted(bases + (instance,)): + func = getattr(base, '_mixin_setup', None) + if func is not None and func not in instance._mixin_setup_funcs: + instance._mixin_setup_funcs.append(func) + + func = getattr(base, '_mixin_after_parsed', None) + if func is not None and func not in instance._mixin_after_parsed_funcs: + instance._mixin_after_parsed_funcs.append(func) + + # Mark process_ functions with the base priority for sorting + for func in dir(base): + if not func.startswith('process_'): + continue + func = getattr(base, func) + if getattr(func, '_mixin_prio_', None) is not None: + # Function already has the attribute set, don't override it + continue + func.__func__._mixin_prio_ = getattr(base, '_mixin_prio_', 1000) + + return instance + + +class OptionParser(optparse.OptionParser): + usage = '%prog' + + epilog = ('You can find additional help about %prog issuing "man %prog" ' + 'or on http://docs.saltstack.org/en/latest/index.html') + description = None + + # Private attributes + _mixin_prio_ = 100 + + def __init__(self, *args, **kwargs): + kwargs.setdefault('version', '%prog {0}'.format(version.__version__)) + kwargs.setdefault('usage', self.usage) + if self.description: + kwargs.setdefault('description', self.description) + + if self.epilog: + kwargs.setdefault('epilog', self.epilog) + + optparse.OptionParser.__init__(self, *args, **kwargs) + + if '%prog' in self.epilog: + self.epilog = self.epilog.replace('%prog', self.get_prog_name()) + + def parse_args(self, args=None, values=None): + options, args = optparse.OptionParser.parse_args(self, args, values) + if options.versions_report: + self.print_versions_report() + + self.options, self.args = options, args + + # Gather and run the process_