diff --git a/salt/modules/hashutil.py b/salt/modules/hashutil.py index 7f521511300f..65a7947837fb 100644 --- a/salt/modules/hashutil.py +++ b/salt/modules/hashutil.py @@ -8,10 +8,69 @@ import base64 import hashlib import hmac +import StringIO -# Import third-party libs -import salt.utils +# Import Salt libs +import salt.exceptions import salt.ext.six as six +import salt.utils + + +def digest(instr, checksum='md5'): + ''' + Return a checksum digest for a string + + instr + A string + checksum : ``md5`` + The hashing algorithm to use to generate checksums. Valid options: md5, + sha256, sha512. + + CLI Example: + + .. code-block:: bash + + salt '*' hashutil.digest 'get salted' + ''' + hashing_funcs = { + 'md5': __salt__['hashutil.md5_digest'], + 'sha256': __salt__['hashutil.sha256_digest'], + 'sha512': __salt__['hashutil.sha512_digest'], + } + hash_func = hashing_funcs.get(checksum) + + if hash_func is None: + raise salt.exceptions.CommandExecutionError( + "Hash func '{0}' is not supported.".format(checksum)) + + return hash_func(instr) + + +def digest_file(infile, checksum='md5'): + ''' + Return a checksum digest for a file + + infile + A file path + checksum : ``md5`` + The hashing algorithm to use to generate checksums. Wraps the + :py:func:`hashutil.digest ` execution + function. + + CLI Example: + + .. code-block:: bash + + salt '*' hashutil.digest_file /path/to/file + ''' + if not __salt__['file.file_exists'](infile): + raise salt.exceptions.CommandExecutionError( + "File path '{0}' not found.".format(infile)) + + with open(infile, 'rb') as f: + file_hash = __salt__['hashutil.digest'](f.read(), checksum) + + return file_hash def base64_b64encode(instr): @@ -81,6 +140,39 @@ def base64_encodestring(instr): return base64.encodestring(instr) +def base64_encodefile(fname): + ''' + Read a file from the file system and return as a base64 encoded string + + .. versionadded:: Boron + + Pillar example: + + .. code-block:: yaml + + path: + to: + data: | + {{ salt.hashutil.base64_encodefile('/path/to/binary_file') | indent(6) }} + + The :py:func:`file.decode ` state function can be + used to decode this data and write it to disk. + + CLI Example: + + .. code-block:: bash + + salt '*' hashutil.base64_encodefile /path/to/binary_file + ''' + encoded_f = StringIO.StringIO() + + with open(fname, 'rb') as f: + base64.encode(f, encoded_f) + + encoded_f.seek(0) + return encoded_f.read() + + def base64_decodestring(instr): ''' Decode a base64-encoded string using the "legacy" Python interface @@ -91,7 +183,8 @@ def base64_decodestring(instr): .. code-block:: bash - salt '*' hashutil.base64_decodestring 'Z2V0IHNhbHRlZA==\\n' + salt '*' hashutil.base64_decodestring instr='Z2V0IHNhbHRlZAo=' + ''' if six.PY3: b = salt.utils.to_bytes(instr) @@ -103,6 +196,26 @@ def base64_decodestring(instr): return base64.decodestring(instr) +def base64_decodefile(instr, outfile): + r''' + Decode a base64-encoded string and write the result to a file + + .. versionadded:: 2015.2.0 + + CLI Example: + + .. code-block:: bash + + salt '*' hashutil.base64_decodefile instr='Z2V0IHNhbHRlZAo=' outfile='/path/to/binary_file' + ''' + encoded_f = StringIO.StringIO(instr) + + with open(outfile, 'wb') as f: + base64.decode(encoded_f, f) + + return True + + def md5_digest(instr): ''' Generate an md5 hash of a given string diff --git a/salt/states/file.py b/salt/states/file.py index df1a8129895f..2630eb1fff21 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -4610,3 +4610,116 @@ def mod_run_check_cmd(cmd, filename, **check_cmd_opts): # No reason to stop, return True return True + + +def decode(name, + encoded_data=None, + contents_pillar=None, + encoding_type='base64', + checksum='md5'): + ''' + Decode an encoded file and write it to disk + + .. versionadded:: Boron + + name + Path of the file to be written. + encoded_data + The encoded file. Either this option or ``contents_pillar`` must be + specified. + contents_pillar + A Pillar path to the encoded file. Uses the same path syntax as + :py:func:`pillar.get `. The + :py:func:`hashutil.base64_encodefile + ` function can load encoded + content into Pillar. Either this option or ``encoded_data`` must be + specified. + encoding_type : ``base64`` + The type of encoding. + checksum : ``md5`` + The hashing algorithm to use to generate checksums. Wraps the + :py:func:`hashutil.digest ` execution + function. + + Usage: + + .. code-block:: yaml + + write_base64_encoded_string_to_a_file: + file.decode: + - name: /tmp/new_file + - encoding_type: base64 + - contents_pillar: mypillar:thefile + + # or + + write_base64_encoded_string_to_a_file: + file.decode: + - name: /tmp/new_file + - encoding_type: base64 + - encoded_data: | + Z2V0IHNhbHRlZAo= + + Be careful with multi-line strings that the YAML indentation is correct. + E.g., + + .. code-block:: yaml + + write_base64_encoded_string_to_a_file: + file.decode: + - name: /tmp/new_file + - encoding_type: base64 + - encoded_data: | + {{ salt.pillar.get('path:to:data') | indent(8) }} + ''' + ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''} + + if not (encoded_data or contents_pillar): + raise CommandExecutionError("Specify either the 'encoded_data' or " + "'contents_pillar' argument.") + elif encoded_data and contents_pillar: + raise CommandExecutionError("Specify only one 'encoded_data' or " + "'contents_pillar' argument.") + elif encoded_data: + content = encoded_data + elif contents_pillar: + content = __salt__['pillar.get'](contents_pillar, False) + if content is False: + raise CommandExecutionError('Pillar data not found.') + else: + raise CommandExecutionError('No contents given.') + + dest_exists = __salt__['file.file_exists'](name) + if dest_exists: + instr = __salt__['hashutil.base64_decodestring'](content) + insum = __salt__['hashutil.digest'](instr, checksum) + del instr # no need to keep in-memory after we have the hash + outsum = __salt__['hashutil.digest_file'](name, checksum) + + if insum != outsum: + ret['changes'] = { + 'old': outsum, + 'new': insum, + } + + if not ret['changes']: + ret['comment'] = 'File is in the correct state.' + ret['result'] = True + + return ret + + if __opts__['test'] is True: + ret['comment'] = 'File is set to be updated.' + ret['result'] = None + return ret + + ret['result'] = __salt__['hashutil.base64_decodefile'](content, name) + ret['comment'] = 'File was updated.' + + if not ret['changes']: + ret['changes'] = { + 'old': None, + 'new': __salt__['hashutil.digest_file'](name, checksum), + } + + return ret