def chfullname(name, fullname): ''' Change the user's Full Name CLI Example: .. code-block:: bash salt '*' user.chfullname foo 'Foo Bar' ''' if isinstance(fullname, string_types): fullname = sdecode(fullname) pre_info = info(name) if not pre_info: raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) if isinstance(pre_info['fullname'], string_types): pre_info['fullname'] = sdecode(pre_info['fullname']) if fullname == pre_info['fullname']: return True _dscl( ['/Users/{0}'.format(name), 'RealName', fullname], # use a 'create' command, because a 'change' command would fail if # current fullname is an empty string. The 'create' will just overwrite # this field. ctype='create') # dscl buffers changes, sleep 1 second before checking if new value # matches desired value time.sleep(1) current = info(name).get('fullname') if isinstance(current, string_types): current = sdecode(current) return current == fullname
def chfullname(name, fullname): ''' Change the user's Full Name CLI Example: .. code-block:: bash salt '*' user.chfullname foo 'Foo Bar' ''' if isinstance(fullname, string_types): fullname = sdecode(fullname) pre_info = info(name) if not pre_info: raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) if isinstance(pre_info['fullname'], string_types): pre_info['fullname'] = sdecode(pre_info['fullname']) if fullname == pre_info['fullname']: return True _dscl( ['/Users/{0}'.format(name), 'RealName', fullname], # use a 'create' command, because a 'change' command would fail if # current fullname is an empty string. The 'create' will just overwrite # this field. ctype='create' ) # dscl buffers changes, sleep 1 second before checking if new value # matches desired value time.sleep(1) current = info(name).get('fullname') if isinstance(current, string_types): current = sdecode(current) return current == fullname
def raw_cron(user): ''' Return the contents of the user's crontab CLI Example: .. code-block:: bash salt '*' cron.raw_cron root ''' if _check_instance_uid_match(user) or __grains__.get('os_family') in ( 'Solaris', 'AIX'): cmd = 'crontab -l' # Preserve line endings lines = sdecode( __salt__['cmd.run_stdout'](cmd, runas=user, rstrip=False, python_shell=False)).splitlines(True) else: cmd = 'crontab -u {0} -l'.format(user) # Preserve line endings lines = sdecode(__salt__['cmd.run_stdout']( cmd, rstrip=False, python_shell=False)).splitlines(True) if len(lines) != 0 and lines[0].startswith( '# DO NOT EDIT THIS FILE - edit the master and reinstall.'): del lines[0:3] return ''.join(lines)
def test_sdecode(self): b = '\xe7\xb9\x81\xe4\xbd\x93' if six.PY2 else bytes((0xe7, 0xb9, 0x81, 0xe4, 0xbd, 0x93)) u = u'\u7e41\u4f53' with patch('salt.utils.locales.get_encodings', return_value=['ascii']): self.assertEqual(locales.sdecode(b), b) # no decode with patch('salt.utils.locales.get_encodings', return_value=['utf-8']): self.assertEqual(locales.sdecode(b), u)
def cache_dir(self, path, saltenv='base', include_empty=False, include_pat=None, exclude_pat=None, cachedir=None): ''' Download all of the files in a subdir of the master ''' ret = [] path = self._check_proto(sdecode(path)) # We want to make sure files start with this *directory*, use # '/' explicitly because the master (that's generating the # list of files) only runs on POSIX if not path.endswith('/'): path = path + '/' log.info('Caching directory \'{0}\' for environment \'{1}\''.format( path, saltenv)) # go through the list of all files finding ones that are in # the target directory and caching them for fn_ in self.file_list(saltenv): fn_ = sdecode(fn_) if fn_.strip() and fn_.startswith(path): if salt.utils.check_include_exclude(fn_, include_pat, exclude_pat): fn_ = self.cache_file(salt.utils.url.create(fn_), saltenv, cachedir=cachedir) if fn_: ret.append(fn_) 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. (So all files in saltenv will be copied) # prefix = '' # else: # prefix = separated[0] if cachedir is None: cachedir = self.opts['cachedir'] elif not os.path.isabs(cachedir): cachedir = os.path.join(self.opts['cachdir'], cachedir) dest = salt.utils.path_join(cachedir, 'files', saltenv) for fn_ in self.file_list_emptydirs(saltenv): fn_ = sdecode(fn_) if fn_.startswith(path): minion_dir = '{0}/{1}'.format(dest, fn_) if not os.path.isdir(minion_dir): os.makedirs(minion_dir) ret.append(minion_dir) return ret
def cache_dir(self, path, saltenv='base', include_empty=False, include_pat=None, exclude_pat=None, cachedir=None): ''' Download all of the files in a subdir of the master ''' ret = [] path = self._check_proto(sdecode(path)) # We want to make sure files start with this *directory*, use # '/' explicitly because the master (that's generating the # list of files) only runs on POSIX if not path.endswith('/'): path = path + '/' log.info( 'Caching directory \'{0}\' for environment \'{1}\''.format( path, saltenv ) ) # go through the list of all files finding ones that are in # the target directory and caching them for fn_ in self.file_list(saltenv): fn_ = sdecode(fn_) if fn_.strip() and fn_.startswith(path): if salt.utils.check_include_exclude( fn_, include_pat, exclude_pat): fn_ = self.cache_file( salt.utils.url.create(fn_), saltenv, cachedir=cachedir) if fn_: ret.append(fn_) 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. (So all files in saltenv will be copied) # prefix = '' # else: # prefix = separated[0] if cachedir is None: cachedir = self.opts['cachedir'] elif not os.path.isabs(cachedir): cachedir = os.path.join(self.opts['cachedir'], cachedir) dest = salt.utils.path_join(cachedir, 'files', saltenv) for fn_ in self.file_list_emptydirs(saltenv): fn_ = sdecode(fn_) if fn_.startswith(path): minion_dir = '{0}/{1}'.format(dest, fn_) if not os.path.isdir(minion_dir): os.makedirs(minion_dir) ret.append(minion_dir) return ret
def test_sdecode(self): b = six.b('\xe7\xb9\x81\xe4\xbd\x93') u = u'\u7e41\u4f53' if six.PY2: # Under Py3, the above `b` as bytes, will never decode as anything even comparable using `ascii` # but no unicode error will be raised, as such, sdecode will return the poorly decoded string with patch('salt.utils.locales.get_encodings', return_value=['ascii']): self.assertEqual(locales.sdecode(b), b) # no decode with patch('salt.utils.locales.get_encodings', return_value=['utf-8']): self.assertEqual(locales.sdecode(b), u)
def create(path, saltenv=None): ''' join `path` and `saltenv` into a 'salt://' URL. ''' if salt.utils.platform.is_windows(): path = salt.utils.sanitize_win_path_string(path) path = sdecode(path) query = u'saltenv={0}'.format(saltenv) if saltenv else '' url = sdecode(urlunparse(('file', '', path, '', query, ''))) return u'salt://{0}'.format(url[len('file:///'):])
def test_sdecode(self): u = 'питон' b = u.encode('utf-8') if six.PY2: # Under Py3, the above `b` as bytes, will never decode as anything even comparable using `ascii` # but no unicode error will be raised, as such, sdecode will return the poorly decoded string with patch('salt.utils.locales.get_encodings', return_value=['ascii']): self.assertEqual(locales.sdecode(b), b) # no decode with patch('salt.utils.locales.get_encodings', return_value=['utf-8']): self.assertEqual(locales.sdecode(b), u) # Non strings are left untouched with patch('salt.utils.locales.get_encodings', return_value=['utf-8']): self.assertEqual(locales.sdecode(1), 1)
def _get_gecos(name): ''' Retrieve GECOS field info and return it in dictionary form ''' gecos_field = pwd.getpwnam(_quote_username(name)).pw_gecos.split(',', 3) if not gecos_field: return {} else: # Assign empty strings for any unspecified trailing GECOS fields while len(gecos_field) < 4: gecos_field.append('') return {'fullname': locales.sdecode(gecos_field[0]), 'roomnumber': locales.sdecode(gecos_field[1]), 'workphone': locales.sdecode(gecos_field[2]), 'homephone': locales.sdecode(gecos_field[3])}
def raw_cron(user): ''' Return the contents of the user's crontab CLI Example: .. code-block:: bash salt '*' cron.raw_cron root ''' # Some OS' do not support specifying user via the `crontab` command if __grains__.get('os_family') in ('Solaris', 'AIX'): cmd = 'crontab -l' # Preserve line endings lines = sdecode(__salt__['cmd.run_stdout'](cmd, runas=user, ignore_retcode=True, rstrip=False, python_shell=False)).splitlines(True) # If Salt is running from same user as requested in cron module we don't need any user switch elif _check_instance_uid_match(user): cmd = 'crontab -l' # Preserve line endings lines = sdecode(__salt__['cmd.run_stdout'](cmd, ignore_retcode=True, rstrip=False, python_shell=False)).splitlines(True) # If Salt is running from root user it could modify any user's crontab elif _check_instance_uid_match('root'): cmd = 'crontab -u {0} -l'.format(user) # Preserve line endings lines = sdecode(__salt__['cmd.run_stdout'](cmd, ignore_retcode=True, rstrip=False, python_shell=False)).splitlines(True) # Edge cases here, let's try do a runas else: cmd = 'crontab -l' # Preserve line endings lines = sdecode(__salt__['cmd.run_stdout'](cmd, runas=user, ignore_retcode=True, rstrip=False, python_shell=False)).splitlines(True) if len(lines) != 0 and lines[0].startswith('# DO NOT EDIT THIS FILE - edit the master and reinstall.'): del lines[0:3] return ''.join(lines)
def dir_list(self, saltenv='base', prefix='', env=None): ''' List the dirs in the file_roots with optional relative prefix path to limit directory traversal ''' if env is not None: salt.utils.warn_until( 'Boron', 'Passing a salt environment should be done using \'saltenv\' ' 'not \'env\'. This functionality will be removed in Salt ' 'Boron.' ) # Backwards compatibility saltenv = env ret = [] if saltenv not in self.opts['file_roots']: return ret prefix = prefix.strip('/') for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk( os.path.join(path, prefix), followlinks=True ): ret.append(sdecode(os.path.relpath(root, path))) return ret
def file_list(self, saltenv='base', prefix='', env=None): ''' Return a list of files in the given environment with optional relative prefix path to limit directory traversal ''' if env is not None: salt.utils.warn_until( 'Carbon', 'Passing a salt environment should be done using \'saltenv\' ' 'not \'env\'. This functionality will be removed in Salt ' 'Carbon.') # Backwards compatibility saltenv = env ret = [] if saltenv not in self.opts['file_roots']: return ret prefix = prefix.strip('/') for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk(os.path.join(path, prefix), followlinks=True): for fname in files: relpath = os.path.relpath(os.path.join(root, fname), path) ret.append(sdecode(relpath)) return ret
def file_list_emptydirs(self, saltenv='base', prefix='', env=None): ''' List the empty dirs in the file_roots with optional relative prefix path to limit directory traversal ''' if env is not None: salt.utils.warn_until( 'Boron', 'Passing a salt environment should be done using \'saltenv\' ' 'not \'env\'. This functionality will be removed in Salt ' 'Boron.' ) # Backwards compatibility saltenv = env ret = [] prefix = prefix.strip('/') if saltenv not in self.opts['file_roots']: return ret for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk( os.path.join(path, prefix), followlinks=True ): if len(dirs) == 0 and len(files) == 0: ret.append(sdecode(os.path.relpath(root, path))) return ret
def file_list(self, saltenv='base', prefix=''): ''' List the files on the master ''' load = {'saltenv': saltenv, 'prefix': prefix, 'cmd': '_file_list'} return [sdecode(fn_) for fn_ in self.channel.send(load)]
def _get_gecos(name): ''' Retrieve GECOS field info and return it in dictionary form ''' try: gecos_field = pwd.getpwnam(name).pw_gecos.split(',', 3) except KeyError: raise CommandExecutionError( 'User \'{0}\' does not exist'.format(name) ) if not gecos_field: return {} else: # Assign empty strings for any unspecified trailing GECOS fields while len(gecos_field) < 4: gecos_field.append('') return {'fullname': locales.sdecode(gecos_field[0]), 'roomnumber': locales.sdecode(gecos_field[1]), 'workphone': locales.sdecode(gecos_field[2]), 'homephone': locales.sdecode(gecos_field[3])}
def _get_gecos(name): ''' Retrieve GECOS field info and return it in dictionary form ''' try: gecos_field = pwd.getpwnam(name).pw_gecos.split(',', 3) except KeyError: raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) if not gecos_field: return {} else: # Assign empty strings for any unspecified trailing GECOS fields while len(gecos_field) < 4: gecos_field.append('') return { 'fullname': locales.sdecode(gecos_field[0]), 'roomnumber': locales.sdecode(gecos_field[1]), 'workphone': locales.sdecode(gecos_field[2]), 'homephone': locales.sdecode(gecos_field[3]) }
def dir_list(self, saltenv='base', prefix=''): ''' List the dirs in the file_roots with optional relative prefix path to limit directory traversal ''' ret = [] if saltenv not in self.opts['file_roots']: return ret prefix = prefix.strip('/') for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk(os.path.join(path, prefix), followlinks=True): ret.append(sdecode(os.path.relpath(root, path))) return ret
def dir_list(self, saltenv='base', prefix=''): ''' List the dirs in the file_roots with optional relative prefix path to limit directory traversal ''' ret = [] if saltenv not in self.opts['file_roots']: return ret prefix = prefix.strip('/') for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk( os.path.join(path, prefix), followlinks=True ): ret.append(sdecode(os.path.relpath(root, path))) return ret
def file_list(self, saltenv='base', prefix=''): ''' Return a list of files in the given environment with optional relative prefix path to limit directory traversal ''' ret = [] if saltenv not in self.opts['file_roots']: return ret prefix = prefix.strip('/') for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk(os.path.join(path, prefix), followlinks=True): for fname in files: relpath = os.path.relpath(os.path.join(root, fname), path) ret.append(sdecode(relpath)) return ret
def file_list(self, saltenv='base', prefix='', env=None): ''' List the files on the master ''' if env is not None: salt.utils.warn_until( 'Carbon', 'Passing a salt environment should be done using \'saltenv\' ' 'not \'env\'. This functionality will be removed in Salt ' 'Carbon.') # Backwards compatibility saltenv = env load = {'saltenv': saltenv, 'prefix': prefix, 'cmd': '_file_list'} return [sdecode(fn_) for fn_ in self.channel.send(load)]
def file_list_emptydirs(self, saltenv='base', prefix=''): ''' List the empty dirs in the file_roots with optional relative prefix path to limit directory traversal ''' ret = [] prefix = prefix.strip('/') if saltenv not in self.opts['file_roots']: return ret for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk( os.path.join(path, prefix), followlinks=True ): # Don't walk any directories that match file_ignore_regex or glob dirs[:] = [d for d in dirs if not salt.fileserver.is_file_ignored(self.opts, d)] if len(dirs) == 0 and len(files) == 0: ret.append(sdecode(os.path.relpath(root, path))) return ret
def file_list(self, saltenv='base', prefix='', env=None): ''' List the files on the master ''' if env is not None: salt.utils.warn_until( 'Boron', 'Passing a salt environment should be done using \'saltenv\' ' 'not \'env\'. This functionality will be removed in Salt ' 'Boron.' ) # Backwards compatibility saltenv = env load = {'saltenv': saltenv, 'prefix': prefix, 'cmd': '_file_list'} return [sdecode(fn_) for fn_ in self.channel.send(load)]
def file_list(self, saltenv='base', prefix=''): ''' Return a list of files in the given environment with optional relative prefix path to limit directory traversal ''' ret = [] if saltenv not in self.opts['file_roots']: return ret prefix = prefix.strip('/') for path in self.opts['file_roots'][saltenv]: for root, dirs, files in os.walk( os.path.join(path, prefix), followlinks=True ): # Don't walk any directories that match file_ignore_regex or glob dirs[:] = [d for d in dirs if not salt.fileserver.is_file_ignored(self.opts, d)] for fname in files: relpath = os.path.relpath(os.path.join(root, fname), path) ret.append(sdecode(relpath)) return ret
def _format_host(host, data): host = sdecode(host) colors = salt.utils.get_colors(__opts__.get('color'), __opts__.get('color_theme')) tabular = __opts__.get('state_tabular', False) rcounts = {} rdurations = [] hcolor = colors['GREEN'] hstrs = [] nchanges = 0 strip_colors = __opts__.get('strip_colors', True) if isinstance(data, int) or isinstance(data, str): # Data in this format is from saltmod.function, # so it is always a 'change' nchanges = 1 hstrs.append((u'{0} {1}{2[ENDC]}'.format(hcolor, data, colors))) hcolor = colors['CYAN'] # Print the minion name in cyan if isinstance(data, list): # Errors have been detected, list them in RED! hcolor = colors['LIGHT_RED'] hstrs.append( (u' {0}Data failed to compile:{1[ENDC]}'.format(hcolor, colors))) for err in data: if strip_colors: err = salt.output.strip_esc_sequence(sdecode(err)) hstrs.append((u'{0}----------\n {1}{2[ENDC]}'.format( hcolor, err, colors))) if isinstance(data, dict): # Verify that the needed data is present data_tmp = {} for tname, info in six.iteritems(data): if isinstance( info, dict ) and tname is not 'changes' and info and '__run_num__' not in info: err = (u'The State execution failed to record the order ' 'in which all states were executed. The state ' 'return missing data is:') hstrs.insert(0, pprint.pformat(info)) hstrs.insert(0, err) if isinstance(info, dict) and 'result' in info: data_tmp[tname] = info data = data_tmp # Everything rendered as it should display the output for tname in sorted(data, key=lambda k: data[k].get('__run_num__', 0)): ret = data[tname] # Increment result counts rcounts.setdefault(ret['result'], 0) rcounts[ret['result']] += 1 rduration = ret.get('duration', 0) try: float(rduration) rdurations.append(rduration) except ValueError: rduration, _, _ = rduration.partition(' ms') try: float(rduration) rdurations.append(rduration) except ValueError: log.error('Cannot parse a float from duration {0}'.format( ret.get('duration', 0))) tcolor = colors['GREEN'] orchestration = ret.get('__orchestration__', False) schanged, ctext = _format_changes(ret['changes'], orchestration) nchanges += 1 if schanged else 0 # Skip this state if it was successful & diff output was requested if __opts__.get('state_output_diff', False) and \ ret['result'] and not schanged: continue # Skip this state if state_verbose is False, the result is True and # there were no changes made if not __opts__.get('state_verbose', False) and \ ret['result'] and not schanged: continue if schanged: tcolor = colors['CYAN'] if ret['result'] is False: hcolor = colors['RED'] tcolor = colors['RED'] if ret['result'] is None: hcolor = colors['LIGHT_YELLOW'] tcolor = colors['LIGHT_YELLOW'] comps = [sdecode(comp) for comp in tname.split('_|-')] if __opts__.get('state_output', 'full').lower() == 'filter': # By default, full data is shown for all types. However, return # data may be excluded by setting state_output_exclude to a # comma-separated list of True, False or None, or including the # same list with the exclude option on the command line. For # now, this option must include a comma. For example: # exclude=True, # The same functionality is also available for making return # data terse, instead of excluding it. cliargs = __opts__.get('arg', []) clikwargs = {} for item in cliargs: if isinstance(item, dict) and '__kwarg__' in item: clikwargs = item.copy() exclude = clikwargs.get( 'exclude', __opts__.get('state_output_exclude', [])) if isinstance(exclude, six.string_types): exclude = str(exclude).split(',') terse = clikwargs.get('terse', __opts__.get('state_output_terse', [])) if isinstance(terse, six.string_types): terse = str(terse).split(',') if str(ret['result']) in terse: msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue if str(ret['result']) in exclude: continue elif __opts__.get('state_output', 'full').lower() == 'terse': # Print this chunk in a terse way and continue in the # loop msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue elif __opts__.get('state_output', 'full').lower().startswith('mixed'): if __opts__['state_output'] == 'mixed_id': # Swap in the ID for the name. Refs #35137 comps[2] = comps[1] # Print terse unless it failed if ret['result'] is not False: msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue elif __opts__.get('state_output', 'full').lower() == 'changes': # Print terse if no error and no changes, otherwise, be # verbose if ret['result'] and not schanged: msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue state_lines = [ u'{tcolor}----------{colors[ENDC]}', u' {tcolor} ID: {comps[1]}{colors[ENDC]}', u' {tcolor}Function: {comps[0]}.{comps[3]}{colors[ENDC]}', u' {tcolor} Result: {ret[result]!s}{colors[ENDC]}', u' {tcolor} Comment: {comment}{colors[ENDC]}', ] if __opts__.get('state_output_profile', True) and 'start_time' in ret: state_lines.extend([ u' {tcolor} Started: {ret[start_time]!s}{colors[ENDC]}', u' {tcolor}Duration: {ret[duration]!s}{colors[ENDC]}', ]) # This isn't the prettiest way of doing this, but it's readable. if comps[1] != comps[2]: state_lines.insert( 3, u' {tcolor} Name: {comps[2]}{colors[ENDC]}') # be sure that ret['comment'] is utf-8 friendly try: if not isinstance(ret['comment'], six.text_type): ret['comment'] = str(ret['comment']).decode('utf-8') except UnicodeDecodeError: # but try to continue on errors pass try: comment = sdecode(ret['comment']) comment = comment.strip().replace(u'\n', u'\n' + u' ' * 14) except AttributeError: # Assume comment is a list try: comment = ret['comment'].join(' ').replace( u'\n', u'\n' + u' ' * 13) except AttributeError: # Comment isn't a list either, just convert to string comment = str(ret['comment']) comment = comment.strip().replace(u'\n', u'\n' + u' ' * 14) # If there is a data attribute, append it to the comment if 'data' in ret: if isinstance(ret['data'], list): for item in ret['data']: comment = '{0} {1}'.format(comment, item) elif isinstance(ret['data'], dict): for key, value in ret['data'].items(): comment = '{0}\n\t\t{1}: {2}'.format( comment, key, value) else: comment = '{0} {1}'.format(comment, ret['data']) for detail in ['start_time', 'duration']: ret.setdefault(detail, u'') if ret['duration'] != '': ret['duration'] = u'{0} ms'.format(ret['duration']) svars = { 'tcolor': tcolor, 'comps': comps, 'ret': ret, 'comment': sdecode(comment), # This nukes any trailing \n and indents the others. 'colors': colors } hstrs.extend([sline.format(**svars) for sline in state_lines]) changes = u' Changes: ' + ctext hstrs.append((u'{0}{1}{2[ENDC]}'.format(tcolor, changes, colors))) if 'warnings' in ret: rcounts.setdefault('warnings', 0) rcounts['warnings'] += 1 wrapper = textwrap.TextWrapper(width=80, initial_indent=u' ' * 14, subsequent_indent=u' ' * 14) hstrs.append( u' {colors[LIGHT_RED]} Warnings: {0}{colors[ENDC]}'. format(wrapper.fill('\n'.join(ret['warnings'])).lstrip(), colors=colors)) # Append result counts to end of output colorfmt = u'{0}{1}{2[ENDC]}' rlabel = { True: u'Succeeded', False: u'Failed', None: u'Not Run', 'warnings': u'Warnings' } count_max_len = max([len(str(x)) for x in six.itervalues(rcounts)] or [0]) label_max_len = max([len(x) for x in six.itervalues(rlabel)] or [0]) line_max_len = label_max_len + count_max_len + 2 # +2 for ': ' hstrs.append( colorfmt.format( colors['CYAN'], u'\nSummary for {0}\n{1}'.format(host, '-' * line_max_len), colors)) def _counts(label, count): return u'{0}: {1:>{2}}'.format(label, count, line_max_len - (len(label) + 2)) # Successful states changestats = [] if None in rcounts and rcounts.get(None, 0) > 0: # test=True states changestats.append( colorfmt.format(colors['LIGHT_YELLOW'], u'unchanged={0}'.format(rcounts.get(None, 0)), colors)) if nchanges > 0: changestats.append( colorfmt.format(colors['GREEN'], u'changed={0}'.format(nchanges), colors)) if changestats: changestats = u' ({0})'.format(', '.join(changestats)) else: changestats = u'' hstrs.append( colorfmt.format( colors['GREEN'], _counts(rlabel[True], rcounts.get(True, 0) + rcounts.get(None, 0)), colors) + changestats) # Failed states num_failed = rcounts.get(False, 0) hstrs.append( colorfmt.format(colors['RED'] if num_failed else colors['CYAN'], _counts(rlabel[False], num_failed), colors)) num_warnings = rcounts.get('warnings', 0) if num_warnings: hstrs.append( colorfmt.format(colors['LIGHT_RED'], _counts(rlabel['warnings'], num_warnings), colors)) totals = u'{0}\nTotal states run: {1:>{2}}'.format( '-' * line_max_len, sum(six.itervalues(rcounts)) - rcounts.get('warnings', 0), line_max_len - 7) hstrs.append(colorfmt.format(colors['CYAN'], totals, colors)) if __opts__.get('state_output_profile', True): sum_duration = sum(rdurations) duration_unit = 'ms' # convert to seconds if duration is 1000ms or more if sum_duration > 999: sum_duration /= 1000 duration_unit = 's' total_duration = u'Total run time: {0} {1}'.format( '{0:.3f}'.format(sum_duration).rjust(line_max_len - 5), duration_unit) hstrs.append( colorfmt.format(colors['CYAN'], total_duration, colors)) if strip_colors: host = salt.output.strip_esc_sequence(host) hstrs.insert(0, (u'{0}{1}:{2[ENDC]}'.format(hcolor, host, colors))) return u'\n'.join(hstrs), nchanges > 0
def _format_host(host, data): host = sdecode(host) colors = salt.utils.get_colors( __opts__.get('color'), __opts__.get('color_theme')) tabular = __opts__.get('state_tabular', False) rcounts = {} rdurations = [] hcolor = colors['GREEN'] hstrs = [] nchanges = 0 strip_colors = __opts__.get('strip_colors', True) if isinstance(data, int) or isinstance(data, str): # Data in this format is from saltmod.function, # so it is always a 'change' nchanges = 1 hstrs.append((u'{0} {1}{2[ENDC]}' .format(hcolor, data, colors))) hcolor = colors['CYAN'] # Print the minion name in cyan if isinstance(data, list): # Errors have been detected, list them in RED! hcolor = colors['LIGHT_RED'] hstrs.append((u' {0}Data failed to compile:{1[ENDC]}' .format(hcolor, colors))) for err in data: if strip_colors: err = salt.output.strip_esc_sequence(sdecode(err)) hstrs.append((u'{0}----------\n {1}{2[ENDC]}' .format(hcolor, err, colors))) if isinstance(data, dict): # Verify that the needed data is present data_tmp = {} for tname, info in six.iteritems(data): if isinstance(info, dict) and '__run_num__' not in info: err = (u'The State execution failed to record the order ' 'in which all states were executed. The state ' 'return missing data is:') hstrs.insert(0, pprint.pformat(info)) hstrs.insert(0, err) if isinstance(info, dict) and 'result' in info: data_tmp[tname] = info data = data_tmp # Everything rendered as it should display the output for tname in sorted( data, key=lambda k: data[k].get('__run_num__', 0)): ret = data[tname] # Increment result counts rcounts.setdefault(ret['result'], 0) rcounts[ret['result']] += 1 rdurations.append(ret.get('duration', 0)) tcolor = colors['GREEN'] schanged, ctext = _format_changes(ret['changes']) nchanges += 1 if schanged else 0 # Skip this state if it was successful & diff output was requested if __opts__.get('state_output_diff', False) and \ ret['result'] and not schanged: continue # Skip this state if state_verbose is False, the result is True and # there were no changes made if not __opts__.get('state_verbose', False) and \ ret['result'] and not schanged: continue if schanged: tcolor = colors['CYAN'] if ret['result'] is False: hcolor = colors['RED'] tcolor = colors['RED'] if ret['result'] is None: hcolor = colors['LIGHT_YELLOW'] tcolor = colors['LIGHT_YELLOW'] comps = [sdecode(comp) for comp in tname.split('_|-')] if __opts__.get('state_output', 'full').lower() == 'filter': # By default, full data is shown for all types. However, return # data may be excluded by setting state_output_exclude to a # comma-separated list of True, False or None, or including the # same list with the exclude option on the command line. For # now, this option must include a comma. For example: # exclude=True, # The same functionality is also available for making return # data terse, instead of excluding it. cliargs = __opts__.get('arg', []) clikwargs = {} for item in cliargs: if isinstance(item, dict) and '__kwarg__' in item: clikwargs = item.copy() exclude = clikwargs.get( 'exclude', __opts__.get('state_output_exclude', []) ) if isinstance(exclude, six.string_types): exclude = str(exclude).split(',') terse = clikwargs.get( 'terse', __opts__.get('state_output_terse', []) ) if isinstance(terse, six.string_types): terse = str(terse).split(',') if str(ret['result']) in terse: msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue if str(ret['result']) in exclude: continue elif __opts__.get('state_output', 'full').lower() == 'terse': # Print this chunk in a terse way and continue in the # loop msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue elif __opts__.get('state_output', 'full').lower() == 'mixed': # Print terse unless it failed if ret['result'] is not False: msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue elif __opts__.get('state_output', 'full').lower() == 'changes': # Print terse if no error and no changes, otherwise, be # verbose if ret['result'] and not schanged: msg = _format_terse(tcolor, comps, ret, colors, tabular) hstrs.append(msg) continue state_lines = [ u'{tcolor}----------{colors[ENDC]}', u' {tcolor} ID: {comps[1]}{colors[ENDC]}', u' {tcolor}Function: {comps[0]}.{comps[3]}{colors[ENDC]}', u' {tcolor} Result: {ret[result]!s}{colors[ENDC]}', u' {tcolor} Comment: {comment}{colors[ENDC]}', ] if __opts__.get('state_output_profile', True): state_lines.extend([ u' {tcolor} Started: {ret[start_time]!s}{colors[ENDC]}', u' {tcolor}Duration: {ret[duration]!s}{colors[ENDC]}', ]) # This isn't the prettiest way of doing this, but it's readable. if comps[1] != comps[2]: state_lines.insert( 3, u' {tcolor} Name: {comps[2]}{colors[ENDC]}') # be sure that ret['comment'] is utf-8 friendly try: if not isinstance(ret['comment'], six.text_type): ret['comment'] = ret['comment'].decode('utf-8') except UnicodeDecodeError: # but try to continue on errors pass try: comment = sdecode(ret['comment']) comment = comment.strip().replace( u'\n', u'\n' + u' ' * 14) except AttributeError: # Assume comment is a list try: comment = ret['comment'].join(' ').replace( u'\n', u'\n' + u' ' * 13) except AttributeError: # Comment isn't a list either, just convert to string comment = str(ret['comment']) comment = comment.strip().replace( u'\n', u'\n' + u' ' * 14) # If there is a data attribute, append it to the comment if 'data' in ret: if isinstance(ret['data'], list): for item in ret['data']: comment = '{0} {1}'.format(comment, item) elif isinstance(ret['data'], dict): for key, value in ret['data'].items(): comment = '{0}\n\t\t{1}: {2}'.format(comment, key, value) else: comment = '{0} {1}'.format(comment, ret['data']) for detail in ['start_time', 'duration']: ret.setdefault(detail, u'') if ret['duration'] != '': ret['duration'] = u'{0} ms'.format(ret['duration']) svars = { 'tcolor': tcolor, 'comps': comps, 'ret': ret, 'comment': sdecode(comment), # This nukes any trailing \n and indents the others. 'colors': colors } hstrs.extend([sline.format(**svars) for sline in state_lines]) changes = u' Changes: ' + ctext hstrs.append((u'{0}{1}{2[ENDC]}' .format(tcolor, changes, colors))) if 'warnings' in ret: rcounts.setdefault('warnings', 0) rcounts['warnings'] += 1 wrapper = textwrap.TextWrapper( width=80, initial_indent=u' ' * 14, subsequent_indent=u' ' * 14 ) hstrs.append( u' {colors[LIGHT_RED]} Warnings: {0}{colors[ENDC]}'.format( wrapper.fill('\n'.join(ret['warnings'])).lstrip(), colors=colors ) ) # Append result counts to end of output colorfmt = u'{0}{1}{2[ENDC]}' rlabel = {True: u'Succeeded', False: u'Failed', None: u'Not Run', 'warnings': u'Warnings'} count_max_len = max([len(str(x)) for x in six.itervalues(rcounts)] or [0]) label_max_len = max([len(x) for x in six.itervalues(rlabel)] or [0]) line_max_len = label_max_len + count_max_len + 2 # +2 for ': ' hstrs.append( colorfmt.format( colors['CYAN'], u'\nSummary for {0}\n{1}'.format(host, '-' * line_max_len), colors ) ) def _counts(label, count): return u'{0}: {1:>{2}}'.format( label, count, line_max_len - (len(label) + 2) ) # Successful states changestats = [] if None in rcounts and rcounts.get(None, 0) > 0: # test=True states changestats.append( colorfmt.format( colors['LIGHT_YELLOW'], u'unchanged={0}'.format(rcounts.get(None, 0)), colors ) ) if nchanges > 0: changestats.append( colorfmt.format( colors['GREEN'], u'changed={0}'.format(nchanges), colors ) ) if changestats: changestats = u' ({0})'.format(', '.join(changestats)) else: changestats = u'' hstrs.append( colorfmt.format( colors['GREEN'], _counts( rlabel[True], rcounts.get(True, 0) + rcounts.get(None, 0) ), colors ) + changestats ) # Failed states num_failed = rcounts.get(False, 0) hstrs.append( colorfmt.format( colors['RED'] if num_failed else colors['CYAN'], _counts(rlabel[False], num_failed), colors ) ) num_warnings = rcounts.get('warnings', 0) if num_warnings: hstrs.append( colorfmt.format( colors['LIGHT_RED'], _counts(rlabel['warnings'], num_warnings), colors ) ) totals = u'{0}\nTotal states run: {1:>{2}}'.format('-' * line_max_len, sum(six.itervalues(rcounts)) - rcounts.get('warnings', 0), line_max_len - 7) hstrs.append(colorfmt.format(colors['CYAN'], totals, colors)) if __opts__.get('state_output_profile', False): sum_duration = sum(rdurations) duration_unit = 'ms' # convert to seconds if duration is 1000ms or more if sum_duration > 999: sum_duration /= 1000 duration_unit = 's' total_duration = u'Total run time: {0} {1}'.format( '{0:.3f}'.format(sum_duration).rjust(line_max_len - 5), duration_unit) hstrs.append(colorfmt.format(colors['CYAN'], total_duration, colors)) if strip_colors: host = salt.output.strip_esc_sequence(host) hstrs.insert(0, (u'{0}{1}:{2[ENDC]}'.format(hcolor, host, colors))) return u'\n'.join(hstrs), nchanges > 0
def display(self, minion_id, data): tinyout = [] minion_id = sdecode(minion_id) color = self.colors['CYAN'] # Print the minion name in cyan tinyout.append((u'{0}{1}{2[ENDC]}:'.format(color, minion_id, self.colors))) if isinstance(data, int) or isinstance(data, str): tinyout.append(self._indent(str(data))) if isinstance(data, list): for item in data: tinyout.append(self._indent(item)) if isinstance(data, dict): # Verify that the needed data is present. # Example of valid data: # { # "frghcslsgwv01": { # "file_|-/etc/salt/roster_|-/etc/salt/roster_|-managed": { # "comment": "File /etc/salt/roster is in the correct state", # "pchanges": {}, # "name": "/etc/salt/roster", # "start_time": "14:49:50.431497", # "result": true, # "duration": 3.563, # "__run_num__": 4, # "changes": {}, # "__id__": "/etc/salt/roster" # }, data_tmp = {} for tname, info in six.iteritems(data): if isinstance(info, dict) and tname is not 'changes' \ and info and '__run_num__' not in info: err = (u'The State execution failed to record the order ' 'in which all states were executed. The state ' 'return missing data is:') tinyout.insert(0, pprint.pformat(info)) tinyout.insert(0, err) if isinstance(info, dict) and 'result' in info: data_tmp[tname] = info data = data_tmp issues = 0 for block_key in sorted( data, key=lambda k: data[k].get('__run_num__', 0)): ret = data[block_key] changes = ret['changes'] status = ret['result'] diff_msg = '' if status is True: color = self.colors['GREEN'] status_msg = 'ok' if changes: color = self.colors['CYAN'] status_msg += ' (changed)' else: # status is None when __opts__['test'] has been set by user color = self.colors['LIGHT_YELLOW' \ if status is None else 'RED'] status_msg = 'failed' if 'comment' in ret: status_msg += '\n{0}'.format(ret['comment']) issues += 1 if changes and 'diff' in changes: diff_msg += changes['diff'].rstrip() comps = [sdecode(comp) for comp in block_key.split('_|-')] task_type = comps[0] task_description = comps[2].splitlines() if len(task_description) > 1: task_description = [ u'{0} [...]'.format(task_description[0]) ] tinyout.append(u'{0}- ({1}) {2} ...{3}{4}{5[ENDC]}'.format( ' ' * self.indent, task_type, task_description[0], color, status_msg, self.colors)) if diff_msg: tinyout.append(u'{0}diff:{1[ENDC]}'.format( self.colors['CYAN'], self.colors)) for line in diff_msg.splitlines(): tinyout.append(u'{0}{1}{2[ENDC]}'.format( self.colors['GREEN'], '{0}{1}'.format(' ' * self.indent, line), self.colors)) summary = u'{0} '.format(minion_id) summary += u'has {0} issue(s)'.format(issues) if issues else \ u'is in the required state' color = self.colors['RED' if issues else 'GREEN'] tinyout.append( (u'\t{0}-- {1}{2[ENDC]}'.format(color, summary, self.colors))) return u'\n'.join(tinyout)
def _changes(name, uid=None, gid=None, groups=None, optional_groups=None, remove_groups=True, home=None, createhome=True, password=None, enforce_password=True, empty_password=False, shell=None, fullname='', roomnumber='', workphone='', homephone='', loginclass=None, date=0, mindays=0, maxdays=999999, inactdays=0, warndays=7, expire=None, win_homedrive=None, win_profile=None, win_logonscript=None, win_description=None): ''' Return a dict of the changes required for a user if the user is present, otherwise return False. Updated in 2015.8.0 to include support for windows homedrive, profile, logonscript, and description fields. Updated in 2014.7.0 to include support for shadow attributes, all attributes supported as integers only. ''' if 'shadow.info' in __salt__: lshad = __salt__['shadow.info'](name) lusr = __salt__['user.info'](name) if not lusr: return False change = {} if groups is None: groups = lusr['groups'] wanted_groups = sorted(set((groups or []) + (optional_groups or []))) if uid and lusr['uid'] != uid: change['uid'] = uid if gid is not None and lusr['gid'] not in ( gid, __salt__['file.group_to_gid'](gid)): change['gid'] = gid default_grp = __salt__['file.gid_to_group']( gid if gid is not None else lusr['gid']) # remove the default group from the list for comparison purposes if default_grp in lusr['groups']: lusr['groups'].remove(default_grp) if name in lusr['groups'] and name not in wanted_groups: lusr['groups'].remove(name) # remove default group from wanted_groups, as this requirement is # already met if default_grp in wanted_groups: wanted_groups.remove(default_grp) if _group_changes(lusr['groups'], wanted_groups, remove_groups): change['groups'] = wanted_groups if home and lusr['home'] != home: change['home'] = home if createhome: newhome = home if home else lusr['home'] if newhome is not None and not os.path.isdir(newhome): change['homeDoesNotExist'] = newhome if shell and lusr['shell'] != shell: change['shell'] = shell if 'shadow.info' in __salt__ and 'shadow.default_hash' in __salt__: if password: default_hash = __salt__['shadow.default_hash']() if lshad['passwd'] == default_hash \ or lshad['passwd'] != default_hash and enforce_password: if lshad['passwd'] != password: change['passwd'] = password if date and date is not 0 and lshad['lstchg'] != date: change['date'] = date if mindays and mindays is not 0 and lshad['min'] != mindays: change['mindays'] = mindays if maxdays and maxdays is not 999999 and lshad['max'] != maxdays: change['maxdays'] = maxdays if inactdays and inactdays is not 0 and lshad['inact'] != inactdays: change['inactdays'] = inactdays if warndays and warndays is not 7 and lshad['warn'] != warndays: change['warndays'] = warndays if expire and lshad['expire'] != expire: change['expire'] = expire elif 'shadow.info' in __salt__ and salt.utils.is_windows(): if expire and expire is not -1 and salt.utils.date_format( lshad['expire']) != salt.utils.date_format(expire): change['expire'] = expire # GECOS fields if isinstance(fullname, string_types): fullname = sdecode(fullname) if isinstance(lusr['fullname'], string_types): lusr['fullname'] = sdecode(lusr['fullname']) if fullname is not None and lusr['fullname'] != fullname: change['fullname'] = fullname if win_homedrive and lusr['homedrive'] != win_homedrive: change['homedrive'] = win_homedrive if win_profile and lusr['profile'] != win_profile: change['profile'] = win_profile if win_logonscript and lusr['logonscript'] != win_logonscript: change['logonscript'] = win_logonscript if win_description and lusr['description'] != win_description: change['description'] = win_description # MacOS doesn't have full GECOS support, so check for the "ch" functions # and ignore these parameters if these functions do not exist. if 'user.chroomnumber' in __salt__ \ and roomnumber is not None \ and lusr['roomnumber'] != roomnumber: change['roomnumber'] = roomnumber if 'user.chworkphone' in __salt__ \ and workphone is not None \ and lusr['workphone'] != workphone: change['workphone'] = workphone if 'user.chhomephone' in __salt__ \ and homephone is not None \ and lusr['homephone'] != homephone: change['homephone'] = homephone # OpenBSD/FreeBSD login class if __grains__['kernel'] in ('OpenBSD', 'FreeBSD'): if loginclass: if __salt__['user.get_loginclass'](name) != loginclass: change['loginclass'] = loginclass return change
def present(name, uid=None, gid=None, gid_from_name=False, groups=None, optional_groups=None, remove_groups=True, home=None, createhome=True, password=None, hash_password=False, enforce_password=True, empty_password=False, shell=None, unique=True, system=False, fullname=None, roomnumber=None, workphone=None, homephone=None, loginclass=None, date=None, mindays=None, maxdays=None, inactdays=None, warndays=None, expire=None, win_homedrive=None, win_profile=None, win_logonscript=None, win_description=None): ''' Ensure that the named user is present with the specified properties name The name of the user to manage uid The user id to assign, if left empty then the next available user id will be assigned gid The default group id. Also accepts group name. 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, Default is ``False``. groups A list of groups to assign the user to, pass a list object. If a group specified here does not exist on the minion, the state will fail. If set to the empty list, the user will be removed from all groups except the default group. If unset, salt will assume current groups are still wanted (see issue #28706). optional_groups A list of groups to assign the user to, pass a list object. If a group specified here does not exist on the minion, the state will silently ignore it. NOTE: If the same group is specified in both "groups" and "optional_groups", then it will be assumed to be required and not optional. remove_groups Remove groups that the user is a member of that weren't specified in the state, Default is ``True``. home The custom login directory of user. Uses default value of underlying system if not set. Notice that this directory does not have to exist. This also the location of the home directory to create if createhome is set to True. createhome If False, the home directory will not be created if it doesn't exist. Please note that directories leading up to the home directory will NOT be created, Default is ``True``. password A password hash to set for the user. This field is only supported on Linux, FreeBSD, NetBSD, OpenBSD, and Solaris. If the ``empty_password`` argument is set to ``True`` then ``password`` is ignored. For Windows this is the plain text password. For Linux, the hash can be generated with ``openssl passwd -1``. .. versionchanged:: 0.16.0 BSD support added. hash_password Set to True to hash the clear text password. Default is ``False``. enforce_password Set to False to keep the password from being changed if it has already been set and the password hash differs from what is specified in the "password" field. This option will be ignored if "password" is not specified, Default is ``True``. empty_password Set to True to enable password-less login for user, Default is ``False``. shell The login shell, defaults to the system default shell unique Require a unique UID, Default is ``True``. system Choose UID in the range of FIRST_SYSTEM_UID and LAST_SYSTEM_UID, Default is ``False``. loginclass The login class, defaults to empty (BSD only) User comment field (GECOS) support (currently Linux, BSD, and MacOS only): The below values should be specified as strings to avoid ambiguities when the values are loaded. (Especially the phone and room number fields which are likely to contain numeric data) fullname The user's full name roomnumber The user's room number (not supported in MacOS) workphone The user's work phone number (not supported in MacOS) homephone The user's home phone number (not supported in MacOS) .. versionchanged:: 2014.7.0 Shadow attribute support added. Shadow attributes support (currently Linux only): The below values should be specified as integers. date Date of last change of password, represented in days since epoch (January 1, 1970). mindays The minimum number of days between password changes. maxdays The maximum number of days between password changes. inactdays The number of days after a password expires before an account is locked. warndays Number of days prior to maxdays to warn users. expire Date that account expires, represented in days since epoch (January 1, 1970). The below parameters apply to windows only: win_homedrive (Windows Only) The drive letter to use for the home directory. If not specified the home directory will be a unc path. Otherwise the home directory will be mapped to the specified drive. Must be a letter followed by a colon. Because of the colon, the value must be surrounded by single quotes. ie: - win_homedrive: 'U: .. versionchanged:: 2015.8.0 win_profile (Windows Only) The custom profile directory of the user. Uses default value of underlying system if not set. .. versionchanged:: 2015.8.0 win_logonscript (Windows Only) The full path to the logon script to run when the user logs in. .. versionchanged:: 2015.8.0 win_description (Windows Only) A brief description of the purpose of the users account. .. versionchanged:: 2015.8.0 ''' # First check if a password is set. If password is set, check if # hash_password is True, then hash it. if password and hash_password: log.debug('Hashing a clear text password') password = __salt__['shadow.gen_password'](password) if fullname is not None: fullname = sdecode(fullname) if roomnumber is not None: roomnumber = sdecode(roomnumber) if workphone is not None: workphone = sdecode(workphone) if homephone is not None: homephone = sdecode(homephone) ret = { 'name': name, 'changes': {}, 'result': True, 'comment': 'User {0} is present and up to date'.format(name) } # the comma is used to separate field in GECOS, thus resulting into # salt adding the end of fullname each time this function is called for gecos_field in ['fullname', 'roomnumber', 'workphone', 'homephone']: if isinstance(gecos_field, string_types) and ',' in gecos_field: ret['comment'] = "Unsupported char ',' in {0}".format(gecos_field) ret['result'] = False return ret if groups: missing_groups = [x for x in groups if not __salt__['group.info'](x)] if missing_groups: ret['comment'] = 'The following group(s) are not present: ' \ '{0}'.format(','.join(missing_groups)) ret['result'] = False return ret if optional_groups: present_optgroups = [ x for x in optional_groups if __salt__['group.info'](x) ] for missing_optgroup in [ x for x in optional_groups if x not in present_optgroups ]: log.debug('Optional group "{0}" for user "{1}" is not ' 'present'.format(missing_optgroup, name)) else: present_optgroups = None # Log a warning for all groups specified in both "groups" and # "optional_groups" lists. if groups and optional_groups: for isected in set(groups).intersection(optional_groups): log.warning('Group "{0}" specified in both groups and ' 'optional_groups for user {1}'.format(isected, name)) if gid_from_name: gid = __salt__['file.group_to_gid'](name) if empty_password: __salt__['shadow.del_password'](name) changes = _changes(name, uid, gid, groups, present_optgroups, remove_groups, home, createhome, password, enforce_password, empty_password, shell, fullname, roomnumber, workphone, homephone, loginclass, date, mindays, maxdays, inactdays, warndays, expire, win_homedrive, win_profile, win_logonscript, win_description) if changes: if __opts__['test']: ret['result'] = None ret['comment'] = ('The following user attributes are set to be ' 'changed:\n') for key, val in iteritems(changes): if key == 'password': val = 'XXX-REDACTED-XXX' ret['comment'] += '{0}: {1}\n'.format(key, val) return ret # The user is present if 'shadow.info' in __salt__: lshad = __salt__['shadow.info'](name) if __grains__['kernel'] in ('OpenBSD', 'FreeBSD'): lcpre = __salt__['user.get_loginclass'](name) pre = __salt__['user.info'](name) for key, val in iteritems(changes): if key == 'passwd' and not empty_password: __salt__['shadow.set_password'](name, password) continue if key == 'date': __salt__['shadow.set_date'](name, date) continue # run chhome once to avoid any possible bad side-effect if key == 'home' and 'homeDoesNotExist' not in changes: if __grains__['kernel'] == 'Darwin': __salt__['user.chhome'](name, val) else: __salt__['user.chhome'](name, val, False) continue if key == 'homeDoesNotExist': if __grains__['kernel'] == 'Darwin': __salt__['user.chhome'](name, val) else: __salt__['user.chhome'](name, val, True) if not os.path.isdir(val): __salt__['file.mkdir'](val, pre['uid'], pre['gid'], 0o755) continue if key == 'mindays': __salt__['shadow.set_mindays'](name, mindays) continue if key == 'maxdays': __salt__['shadow.set_maxdays'](name, maxdays) continue if key == 'inactdays': __salt__['shadow.set_inactdays'](name, inactdays) continue if key == 'warndays': __salt__['shadow.set_warndays'](name, warndays) continue if key == 'expire': __salt__['shadow.set_expire'](name, expire) continue if key == 'win_homedrive': __salt__['user.update'](name=name, homedrive=val) continue if key == 'win_profile': __salt__['user.update'](name=name, profile=val) continue if key == 'win_logonscript': __salt__['user.update'](name=name, logonscript=val) continue if key == 'win_description': __salt__['user.update'](name=name, description=val) continue if key == 'groups': __salt__['user.ch{0}'.format(key)](name, val, not remove_groups) else: __salt__['user.ch{0}'.format(key)](name, val) post = __salt__['user.info'](name) spost = {} if 'shadow.info' in __salt__ and lshad['passwd'] != password: spost = __salt__['shadow.info'](name) if __grains__['kernel'] in ('OpenBSD', 'FreeBSD'): lcpost = __salt__['user.get_loginclass'](name) # See if anything changed for key in post: if post[key] != pre[key]: ret['changes'][key] = post[key] if 'shadow.info' in __salt__: for key in spost: if lshad[key] != spost[key]: if key == 'passwd': ret['changes'][key] = 'XXX-REDACTED-XXX' else: ret['changes'][key] = spost[key] if __grains__['kernel'] in ('OpenBSD', 'FreeBSD') and lcpost != lcpre: ret['changes']['loginclass'] = lcpost if ret['changes']: ret['comment'] = 'Updated user {0}'.format(name) changes = _changes(name, uid, gid, groups, present_optgroups, remove_groups, home, createhome, password, enforce_password, empty_password, shell, fullname, roomnumber, workphone, homephone, loginclass, date, mindays, maxdays, inactdays, warndays, expire, win_homedrive, win_profile, win_logonscript, win_description) if changes: ret['comment'] = 'These values could not be changed: {0}'.format( changes) ret['result'] = False return ret if changes is False: # The user is not present, make it! if __opts__['test']: ret['result'] = None ret['comment'] = 'User {0} set to be added'.format(name) return ret if groups and present_optgroups: groups.extend(present_optgroups) elif present_optgroups: groups = present_optgroups[:] # Setup params specific to Linux and Windows to be passed to the # add.user function if not salt.utils.is_windows(): params = { 'name': name, 'uid': uid, 'gid': gid, 'groups': groups, 'home': home, 'shell': shell, 'unique': unique, 'system': system, 'fullname': fullname, 'roomnumber': roomnumber, 'workphone': workphone, 'homephone': homephone, 'createhome': createhome, 'loginclass': loginclass } else: params = ({ 'name': name, 'password': password, 'fullname': fullname, 'description': win_description, 'groups': groups, 'home': home, 'homedrive': win_homedrive, 'profile': win_profile, 'logonscript': win_logonscript }) if __salt__['user.add'](**params): ret['comment'] = 'New user {0} created'.format(name) ret['changes'] = __salt__['user.info'](name) if not createhome: # pwd incorrectly reports presence of home ret['changes']['home'] = '' if 'shadow.info' in __salt__ \ and not salt.utils.is_windows()\ and not salt.utils.is_darwin(): if password and not empty_password: __salt__['shadow.set_password'](name, password) spost = __salt__['shadow.info'](name) if spost['passwd'] != password: ret['comment'] = 'User {0} created but failed to set' \ ' password to' \ ' {1}'.format(name, 'XXX-REDACTED-XXX') ret['result'] = False ret['changes']['password'] = '******' if date: __salt__['shadow.set_date'](name, date) spost = __salt__['shadow.info'](name) if spost['lstchg'] != date: ret['comment'] = 'User {0} created but failed to set' \ ' last change date to' \ ' {1}'.format(name, date) ret['result'] = False ret['changes']['date'] = date if mindays: __salt__['shadow.set_mindays'](name, mindays) spost = __salt__['shadow.info'](name) if spost['min'] != mindays: ret['comment'] = 'User {0} created but failed to set' \ ' minimum days to' \ ' {1}'.format(name, mindays) ret['result'] = False ret['changes']['mindays'] = mindays if maxdays: __salt__['shadow.set_maxdays'](name, maxdays) spost = __salt__['shadow.info'](name) if spost['max'] != maxdays: ret['comment'] = 'User {0} created but failed to set' \ ' maximum days to' \ ' {1}'.format(name, maxdays) ret['result'] = False ret['changes']['maxdays'] = maxdays if inactdays: __salt__['shadow.set_inactdays'](name, inactdays) spost = __salt__['shadow.info'](name) if spost['inact'] != inactdays: ret['comment'] = 'User {0} created but failed to set' \ ' inactive days to' \ ' {1}'.format(name, inactdays) ret['result'] = False ret['changes']['inactdays'] = inactdays if warndays: __salt__['shadow.set_warndays'](name, warndays) spost = __salt__['shadow.info'](name) if spost['warn'] != warndays: ret['comment'] = 'User {0} created but failed to set' \ ' warn days to' \ ' {1}'.format(name, warndays) ret['result'] = False ret['changes']['warndays'] = warndays if expire: __salt__['shadow.set_expire'](name, expire) spost = __salt__['shadow.info'](name) if spost['expire'] != expire: ret['comment'] = 'User {0} created but failed to set' \ ' expire days to' \ ' {1}'.format(name, expire) ret['result'] = False ret['changes']['expire'] = expire elif salt.utils.is_windows(): if password and not empty_password: if not __salt__['user.setpassword'](name, password): ret['comment'] = 'User {0} created but failed to set' \ ' password to' \ ' {1}'.format(name, 'XXX-REDACTED-XXX') ret['result'] = False ret['changes']['passwd'] = 'XXX-REDACTED-XXX' if expire: __salt__['shadow.set_expire'](name, expire) spost = __salt__['shadow.info'](name) if salt.utils.date_format( spost['expire']) != salt.utils.date_format(expire): ret['comment'] = 'User {0} created but failed to set' \ ' expire days to' \ ' {1}'.format(name, expire) ret['result'] = False ret['changes']['expiration_date'] = spost['expire'] elif salt.utils.is_darwin() and password and not empty_password: if not __salt__['shadow.set_password'](name, password): ret['comment'] = 'User {0} created but failed to set' \ ' password to' \ ' {1}'.format(name, 'XXX-REDACTED-XXX') ret['result'] = False ret['changes']['passwd'] = 'XXX-REDACTED-XXX' else: ret['comment'] = 'Failed to create new user {0}'.format(name) ret['result'] = False return ret
if u'SETUP_DIRNAME' in globals(): # This is from the exec() call in Salt's setup.py __THIS_FILE = os.path.join(SETUP_DIRNAME, u'salt', u'syspaths.py') # pylint: disable=E0602 else: __THIS_FILE = __file__ # These values are always relative to salt's installation directory INSTALL_DIR = os.path.dirname(os.path.realpath(__THIS_FILE)) CLOUD_DIR = os.path.join(INSTALL_DIR, u'cloud') BOOTSTRAP = os.path.join(CLOUD_DIR, u'deploy', u'bootstrap-salt.sh') ROOT_DIR = __generated_syspaths.ROOT_DIR if ROOT_DIR is None: # The installation time value was not provided, let's define the default if __PLATFORM.startswith(u'win'): ROOT_DIR = sdecode( r'c:\salt') # future lint: disable=non-unicode-string else: ROOT_DIR = u'/' CONFIG_DIR = __generated_syspaths.CONFIG_DIR if CONFIG_DIR is None: if __PLATFORM.startswith(u'win'): CONFIG_DIR = os.path.join(ROOT_DIR, u'conf') elif u'freebsd' in __PLATFORM: CONFIG_DIR = os.path.join(ROOT_DIR, u'usr', u'local', u'etc', u'salt') elif u'netbsd' in __PLATFORM: CONFIG_DIR = os.path.join(ROOT_DIR, u'usr', u'pkg', u'etc', u'salt') elif u'sunos5' in __PLATFORM: CONFIG_DIR = os.path.join(ROOT_DIR, u'opt', u'local', u'etc', u'salt') else: CONFIG_DIR = os.path.join(ROOT_DIR, u'etc', u'salt')
def output(data, **kwargs): # pylint: disable=unused-argument ''' Read in the dict structure generated by the salt key API methods and print the structure. ''' color = salt.utils.color.get_colors(__opts__.get('color'), __opts__.get('color_theme')) strip_colors = __opts__.get('strip_colors', True) ident = 0 if __opts__.get('__multi_key'): ident = 4 if __opts__['transport'] in ('zeromq', 'tcp'): acc = 'minions' pend = 'minions_pre' den = 'minions_denied' rej = 'minions_rejected' cmap = { pend: color['RED'], acc: color['GREEN'], den: color['MAGENTA'], rej: color['BLUE'], 'local': color['MAGENTA'] } trans = { pend: u'{0}{1}Unaccepted Keys:{2}'.format(' ' * ident, color['LIGHT_RED'], color['ENDC']), acc: u'{0}{1}Accepted Keys:{2}'.format(' ' * ident, color['LIGHT_GREEN'], color['ENDC']), den: u'{0}{1}Denied Keys:{2}'.format(' ' * ident, color['LIGHT_MAGENTA'], color['ENDC']), rej: u'{0}{1}Rejected Keys:{2}'.format(' ' * ident, color['LIGHT_BLUE'], color['ENDC']), 'local': u'{0}{1}Local Keys:{2}'.format(' ' * ident, color['LIGHT_MAGENTA'], color['ENDC']) } else: acc = 'accepted' pend = 'pending' rej = 'rejected' cmap = { pend: color['RED'], acc: color['GREEN'], rej: color['BLUE'], 'local': color['MAGENTA'] } trans = { pend: u'{0}{1}Unaccepted Keys:{2}'.format(' ' * ident, color['LIGHT_RED'], color['ENDC']), acc: u'{0}{1}Accepted Keys:{2}'.format(' ' * ident, color['LIGHT_GREEN'], color['ENDC']), rej: u'{0}{1}Rejected Keys:{2}'.format(' ' * ident, color['LIGHT_BLUE'], color['ENDC']), 'local': u'{0}{1}Local Keys:{2}'.format(' ' * ident, color['LIGHT_MAGENTA'], color['ENDC']) } ret = '' for status in sorted(data): ret += u'{0}\n'.format(trans[status]) for key in sorted(data[status]): key = sdecode(key) skey = salt.output.strip_esc_sequence(key) if strip_colors else key if isinstance(data[status], list): ret += u'{0}{1}{2}{3}\n'.format(' ' * ident, cmap[status], skey, color['ENDC']) if isinstance(data[status], dict): ret += u'{0}{1}{2}: {3}{4}\n'.format(' ' * ident, cmap[status], skey, data[status][key], color['ENDC']) return ret
def _changes(name, uid=None, gid=None, groups=None, optional_groups=None, remove_groups=True, home=None, createhome=True, password=None, enforce_password=True, empty_password=False, shell=None, fullname='', roomnumber='', workphone='', homephone='', loginclass=None, date=0, mindays=0, maxdays=999999, inactdays=0, warndays=7, expire=-1, win_homedrive=None, win_profile=None, win_logonscript=None, win_description=None): ''' Return a dict of the changes required for a user if the user is present, otherwise return False. Updated in 2015.8.0 to include support for windows homedrive, profile, logonscript, and description fields. Updated in 2014.7.0 to include support for shadow attributes, all attributes supported as integers only. ''' if 'shadow.info' in __salt__: lshad = __salt__['shadow.info'](name) lusr = __salt__['user.info'](name) if not lusr: return False change = {} wanted_groups = sorted(set((groups or []) + (optional_groups or []))) if uid and lusr['uid'] != uid: change['uid'] = uid if gid is not None and lusr['gid'] not in (gid, __salt__['file.group_to_gid'](gid)): change['gid'] = gid default_grp = __salt__['file.gid_to_group']( gid if gid is not None else lusr['gid'] ) # remove the default group from the list for comparison purposes if default_grp in lusr['groups']: lusr['groups'].remove(default_grp) if name in lusr['groups'] and name not in wanted_groups: lusr['groups'].remove(name) # remove default group from wanted_groups, as this requirement is # already met if default_grp in wanted_groups: wanted_groups.remove(default_grp) if _group_changes(lusr['groups'], wanted_groups, remove_groups): change['groups'] = wanted_groups if home and lusr['home'] != home: change['home'] = home if createhome: newhome = home if home else lusr['home'] if newhome is not None and not os.path.isdir(newhome): change['homeDoesNotExist'] = newhome if shell and lusr['shell'] != shell: change['shell'] = shell if 'shadow.info' in __salt__ and 'shadow.default_hash' in __salt__: if password: default_hash = __salt__['shadow.default_hash']() if lshad['passwd'] == default_hash \ or lshad['passwd'] != default_hash and enforce_password: if lshad['passwd'] != password: change['passwd'] = password if date and date is not 0 and lshad['lstchg'] != date: change['date'] = date if mindays and mindays is not 0 and lshad['min'] != mindays: change['mindays'] = mindays if maxdays and maxdays is not 999999 and lshad['max'] != maxdays: change['maxdays'] = maxdays if inactdays and inactdays is not 0 and lshad['inact'] != inactdays: change['inactdays'] = inactdays if warndays and warndays is not 7 and lshad['warn'] != warndays: change['warndays'] = warndays if expire and expire is not -1 and lshad['expire'] != expire: change['expire'] = expire # GECOS fields if isinstance(fullname, string_types): fullname = sdecode(fullname) if isinstance(lusr['fullname'], string_types): lusr['fullname'] = sdecode(lusr['fullname']) if fullname is not None and lusr['fullname'] != fullname: change['fullname'] = fullname if win_homedrive and lusr['homedrive'] != win_homedrive: change['homedrive'] = win_homedrive if win_profile and lusr['profile'] != win_profile: change['profile'] = win_profile if win_logonscript and lusr['logonscript'] != win_logonscript: change['logonscript'] = win_logonscript if win_description and lusr['description'] != win_description: change['description'] = win_description # MacOS doesn't have full GECOS support, so check for the "ch" functions # and ignore these parameters if these functions do not exist. if 'user.chroomnumber' in __salt__ \ and roomnumber is not None \ and lusr['roomnumber'] != roomnumber: change['roomnumber'] = roomnumber if 'user.chworkphone' in __salt__ \ and workphone is not None \ and lusr['workphone'] != workphone: change['workphone'] = workphone if 'user.chhomephone' in __salt__ \ and homephone is not None \ and lusr['homephone'] != homephone: change['homephone'] = homephone # OpenBSD/FreeBSD login class if __grains__['kernel'] in ('OpenBSD', 'FreeBSD'): if loginclass: if __salt__['user.get_loginclass'](name) != loginclass: change['loginclass'] = loginclass return change
def present(name, uid=None, gid=None, gid_from_name=False, groups=None, optional_groups=None, remove_groups=True, home=None, createhome=True, password=None, enforce_password=True, empty_password=False, shell=None, unique=True, system=False, fullname=None, roomnumber=None, workphone=None, homephone=None, loginclass=None, date=None, mindays=None, maxdays=None, inactdays=None, warndays=None, expire=None, win_homedrive=None, win_profile=None, win_logonscript=None, win_description=None): ''' Ensure that the named user is present with the specified properties name The name of the user to manage uid The user id to assign, if left empty then the next available user id will be assigned gid The default group id. Also accepts group name. 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, Default is ``False``. groups A list of groups to assign the user to, pass a list object. If a group specified here does not exist on the minion, the state will fail. If set to the empty list, the user will be removed from all groups except the default group. optional_groups A list of groups to assign the user to, pass a list object. If a group specified here does not exist on the minion, the state will silently ignore it. NOTE: If the same group is specified in both "groups" and "optional_groups", then it will be assumed to be required and not optional. remove_groups Remove groups that the user is a member of that weren't specified in the state, Default is ``True``. home The custom login directory of user. Uses default value of underlying system if not set. Notice that this directory does not have to exist. This also the location of the home directory to create if createhome is set to True. createhome If False, the home directory will not be created if it doesn't exist. Please note that directories leading up to the home directory will NOT be created, Default is ``True``. password A password hash to set for the user. This field is only supported on Linux, FreeBSD, NetBSD, OpenBSD, and Solaris. If the ``empty_password`` argument is set to ``True`` then ``password`` is ignored. For Windows this is the plain text password. For Linux, the hash can be generated with ``openssl passwd -1``. .. versionchanged:: 0.16.0 BSD support added. enforce_password Set to False to keep the password from being changed if it has already been set and the password hash differs from what is specified in the "password" field. This option will be ignored if "password" is not specified, Default is ``True``. empty_password Set to True to enable password-less login for user, Default is ``False``. shell The login shell, defaults to the system default shell unique Require a unique UID, Default is ``True``. system Choose UID in the range of FIRST_SYSTEM_UID and LAST_SYSTEM_UID, Default is ``False``. loginclass The login class, defaults to empty (BSD only) User comment field (GECOS) support (currently Linux, BSD, and MacOS only): The below values should be specified as strings to avoid ambiguities when the values are loaded. (Especially the phone and room number fields which are likely to contain numeric data) fullname The user's full name roomnumber The user's room number (not supported in MacOS) workphone The user's work phone number (not supported in MacOS) homephone The user's home phone number (not supported in MacOS) .. versionchanged:: 2014.7.0 Shadow attribute support added. Shadow attributes support (currently Linux only): The below values should be specified as integers. date Date of last change of password, represented in days since epoch (January 1, 1970). mindays The minimum number of days between password changes. maxdays The maximum number of days between password changes. inactdays The number of days after a password expires before an account is locked. warndays Number of days prior to maxdays to warn users. expire Date that account expires, represented in days since epoch (January 1, 1970). The below parameters apply to windows only: win_homedrive (Windows Only) The drive letter to use for the home directory. If not specified the home directory will be a unc path. Otherwise the home directory will be mapped to the specified drive. Must be a letter followed by a colon. Because of the colon, the value must be surrounded by single quotes. ie: - win_homedrive: 'U: .. versionchanged:: 2015.8.0 win_profile (Windows Only) The custom profile directory of the user. Uses default value of underlying system if not set. .. versionchanged:: 2015.8.0 win_logonscript (Windows Only) The full path to the logon script to run when the user logs in. .. versionchanged:: 2015.8.0 win_description (Windows Only) A brief description of the purpose of the users account. .. versionchanged:: 2015.8.0 ''' if fullname is not None: fullname = sdecode(fullname) if roomnumber is not None: roomnumber = sdecode(roomnumber) if workphone is not None: workphone = sdecode(workphone) if homephone is not None: homephone = sdecode(homephone) ret = {'name': name, 'changes': {}, 'result': True, 'comment': 'User {0} is present and up to date'.format(name)} # the comma is used to separate field in GECOS, thus resulting into # salt adding the end of fullname each time this function is called for gecos_field in ['fullname', 'roomnumber', 'workphone', 'homephone']: if isinstance(gecos_field, string_types) and ',' in gecos_field: ret['comment'] = "Unsupported char ',' in {0}".format(gecos_field) ret['result'] = False return ret if groups: missing_groups = [x for x in groups if not __salt__['group.info'](x)] if missing_groups: ret['comment'] = 'The following group(s) are not present: ' \ '{0}'.format(','.join(missing_groups)) ret['result'] = False return ret if optional_groups: present_optgroups = [x for x in optional_groups if __salt__['group.info'](x)] for missing_optgroup in [x for x in optional_groups if x not in present_optgroups]: log.debug('Optional group "{0}" for user "{1}" is not ' 'present'.format(missing_optgroup, name)) else: present_optgroups = None # Log a warning for all groups specified in both "groups" and # "optional_groups" lists. if groups and optional_groups: for isected in set(groups).intersection(optional_groups): log.warning('Group "{0}" specified in both groups and ' 'optional_groups for user {1}'.format(isected, name)) if gid_from_name: gid = __salt__['file.group_to_gid'](name) if empty_password: __salt__['shadow.del_password'](name) changes = _changes(name, uid, gid, groups, present_optgroups, remove_groups, home, createhome, password, enforce_password, empty_password, shell, fullname, roomnumber, workphone, homephone, loginclass, date, mindays, maxdays, inactdays, warndays, expire, win_homedrive, win_profile, win_logonscript, win_description) if changes: if __opts__['test']: ret['result'] = None ret['comment'] = ('The following user attributes are set to be ' 'changed:\n') for key, val in iteritems(changes): if key == 'password': val = 'XXX-REDACTED-XXX' ret['comment'] += '{0}: {1}\n'.format(key, val) return ret # The user is present if 'shadow.info' in __salt__: lshad = __salt__['shadow.info'](name) if __grains__['kernel'] in ('OpenBSD', 'FreeBSD'): lcpre = __salt__['user.get_loginclass'](name) pre = __salt__['user.info'](name) for key, val in iteritems(changes): if key == 'passwd' and not empty_password: __salt__['shadow.set_password'](name, password) continue if key == 'date': __salt__['shadow.set_date'](name, date) continue # run chhome once to avoid any possible bad side-effect if key == 'home' and 'homeDoesNotExist' not in changes: if __grains__['kernel'] == 'Darwin': __salt__['user.chhome'](name, val) else: __salt__['user.chhome'](name, val, False) continue if key == 'homeDoesNotExist': if __grains__['kernel'] == 'Darwin': __salt__['user.chhome'](name, val) else: __salt__['user.chhome'](name, val, True) if not os.path.isdir(val): __salt__['file.mkdir'](val, pre['uid'], pre['gid'], 0o755) continue if key == 'mindays': __salt__['shadow.set_mindays'](name, mindays) continue if key == 'maxdays': __salt__['shadow.set_maxdays'](name, maxdays) continue if key == 'inactdays': __salt__['shadow.set_inactdays'](name, inactdays) continue if key == 'warndays': __salt__['shadow.set_warndays'](name, warndays) continue if key == 'expire': __salt__['shadow.set_expire'](name, expire) continue if key == 'win_homedrive': __salt__['user.update'](name=name, homedrive=val) continue if key == 'win_profile': __salt__['user.update'](name=name, profile=val) continue if key == 'win_logonscript': __salt__['user.update'](name=name, logonscript=val) continue if key == 'win_description': __salt__['user.update'](name=name, description=val) continue if key == 'groups': __salt__['user.ch{0}'.format(key)]( name, val, not remove_groups ) else: __salt__['user.ch{0}'.format(key)](name, val) post = __salt__['user.info'](name) spost = {} if 'shadow.info' in __salt__ and lshad['passwd'] != password: spost = __salt__['shadow.info'](name) if __grains__['kernel'] in ('OpenBSD', 'FreeBSD'): lcpost = __salt__['user.get_loginclass'](name) # See if anything changed for key in post: if post[key] != pre[key]: ret['changes'][key] = post[key] if 'shadow.info' in __salt__: for key in spost: if lshad[key] != spost[key]: ret['changes'][key] = spost[key] if __grains__['kernel'] in ('OpenBSD', 'FreeBSD') and lcpost != lcpre: ret['changes']['loginclass'] = lcpost if ret['changes']: ret['comment'] = 'Updated user {0}'.format(name) changes = _changes(name, uid, gid, groups, present_optgroups, remove_groups, home, createhome, password, enforce_password, empty_password, shell, fullname, roomnumber, workphone, homephone, loginclass, date, mindays, maxdays, inactdays, warndays, expire, win_homedrive, win_profile, win_logonscript, win_description) if changes: ret['comment'] = 'These values could not be changed: {0}'.format( changes ) ret['result'] = False return ret if changes is False: # The user is not present, make it! if __opts__['test']: ret['result'] = None ret['comment'] = 'User {0} set to be added'.format(name) return ret if groups and present_optgroups: groups.extend(present_optgroups) elif present_optgroups: groups = present_optgroups[:] # Setup params specific to Linux and Windows to be passed to the # add.user function if not salt.utils.is_windows(): params = {'name': name, 'uid': uid, 'gid': gid, 'groups': groups, 'home': home, 'shell': shell, 'unique': unique, 'system': system, 'fullname': fullname, 'roomnumber': roomnumber, 'workphone': workphone, 'homephone': homephone, 'createhome': createhome, 'loginclass': loginclass} else: params = ({'name': name, 'password': password, 'fullname': fullname, 'description': win_description, 'groups': groups, 'home': home, 'homedrive': win_homedrive, 'profile': win_profile, 'logonscript': win_logonscript}) if __salt__['user.add'](**params): ret['comment'] = 'New user {0} created'.format(name) ret['changes'] = __salt__['user.info'](name) if not createhome: # pwd incorrectly reports presence of home ret['changes']['home'] = '' if 'shadow.info' in __salt__ \ and not salt.utils.is_windows()\ and not salt.utils.is_darwin(): if password and not empty_password: __salt__['shadow.set_password'](name, password) spost = __salt__['shadow.info'](name) if spost['passwd'] != password: ret['comment'] = 'User {0} created but failed to set' \ ' password to' \ ' {1}'.format(name, 'XXX-REDACTED-XXX') ret['result'] = False ret['changes']['password'] = '******' if date: __salt__['shadow.set_date'](name, date) spost = __salt__['shadow.info'](name) if spost['lstchg'] != date: ret['comment'] = 'User {0} created but failed to set' \ ' last change date to' \ ' {1}'.format(name, date) ret['result'] = False ret['changes']['date'] = date if mindays: __salt__['shadow.set_mindays'](name, mindays) spost = __salt__['shadow.info'](name) if spost['min'] != mindays: ret['comment'] = 'User {0} created but failed to set' \ ' minimum days to' \ ' {1}'.format(name, mindays) ret['result'] = False ret['changes']['mindays'] = mindays if maxdays: __salt__['shadow.set_maxdays'](name, maxdays) spost = __salt__['shadow.info'](name) if spost['max'] != maxdays: ret['comment'] = 'User {0} created but failed to set' \ ' maximum days to' \ ' {1}'.format(name, maxdays) ret['result'] = False ret['changes']['maxdays'] = maxdays if inactdays: __salt__['shadow.set_inactdays'](name, inactdays) spost = __salt__['shadow.info'](name) if spost['inact'] != inactdays: ret['comment'] = 'User {0} created but failed to set' \ ' inactive days to' \ ' {1}'.format(name, inactdays) ret['result'] = False ret['changes']['inactdays'] = inactdays if warndays: __salt__['shadow.set_warndays'](name, warndays) spost = __salt__['shadow.info'](name) if spost['warn'] != warndays: ret['comment'] = 'User {0} created but failed to set' \ ' warn days to' \ ' {1}'.format(name, warndays) ret['result'] = False ret['changes']['warndays'] = warndays if expire: __salt__['shadow.set_expire'](name, expire) spost = __salt__['shadow.info'](name) if spost['expire'] != expire: ret['comment'] = 'User {0} created but failed to set' \ ' expire days to' \ ' {1}'.format(name, expire) ret['result'] = False ret['changes']['expire'] = expire elif salt.utils.is_windows() and password and not empty_password: if not __salt__['user.setpassword'](name, password): ret['comment'] = 'User {0} created but failed to set' \ ' password to' \ ' {1}'.format(name, 'XXX-REDACTED-XXX') ret['result'] = False ret['changes']['passwd'] = 'XXX-REDACTED-XXX' elif salt.utils.is_darwin() and password and not empty_password: if not __salt__['shadow.set_password'](name, password): ret['comment'] = 'User {0} created but failed to set' \ ' password to' \ ' {1}'.format(name, 'XXX-REDACTED-XXX') ret['result'] = False ret['changes']['passwd'] = 'XXX-REDACTED-XXX' else: ret['comment'] = 'Failed to create new user {0}'.format(name) ret['result'] = False return ret
def output(data, **kwargs): # pylint: disable=unused-argument ''' Read in the dict structure generated by the salt key API methods and print the structure. ''' color = salt.utils.get_colors( __opts__.get('color'), __opts__.get('color_theme')) strip_colors = __opts__.get('strip_colors', True) ident = 0 if __opts__.get('__multi_key'): ident = 4 if __opts__['transport'] in ('zeromq', 'tcp'): acc = 'minions' pend = 'minions_pre' den = 'minions_denied' rej = 'minions_rejected' cmap = {pend: color['RED'], acc: color['GREEN'], den: color['MAGENTA'], rej: color['BLUE'], 'local': color['MAGENTA']} trans = {pend: u'{0}{1}Unaccepted Keys:{2}'.format( ' ' * ident, color['LIGHT_RED'], color['ENDC']), acc: u'{0}{1}Accepted Keys:{2}'.format( ' ' * ident, color['LIGHT_GREEN'], color['ENDC']), den: u'{0}{1}Denied Keys:{2}'.format( ' ' * ident, color['LIGHT_MAGENTA'], color['ENDC']), rej: u'{0}{1}Rejected Keys:{2}'.format( ' ' * ident, color['LIGHT_BLUE'], color['ENDC']), 'local': u'{0}{1}Local Keys:{2}'.format( ' ' * ident, color['LIGHT_MAGENTA'], color['ENDC'])} else: acc = 'accepted' pend = 'pending' rej = 'rejected' cmap = {pend: color['RED'], acc: color['GREEN'], rej: color['BLUE'], 'local': color['MAGENTA']} trans = {pend: u'{0}{1}Unaccepted Keys:{2}'.format( ' ' * ident, color['LIGHT_RED'], color['ENDC']), acc: u'{0}{1}Accepted Keys:{2}'.format( ' ' * ident, color['LIGHT_GREEN'], color['ENDC']), rej: u'{0}{1}Rejected Keys:{2}'.format( ' ' * ident, color['LIGHT_BLUE'], color['ENDC']), 'local': u'{0}{1}Local Keys:{2}'.format( ' ' * ident, color['LIGHT_MAGENTA'], color['ENDC'])} ret = '' for status in sorted(data): ret += u'{0}\n'.format(trans[status]) for key in sorted(data[status]): key = sdecode(key) skey = salt.output.strip_esc_sequence(key) if strip_colors else key if isinstance(data[status], list): ret += u'{0}{1}{2}{3}\n'.format( ' ' * ident, cmap[status], skey, color['ENDC']) if isinstance(data[status], dict): ret += u'{0}{1}{2}: {3}{4}\n'.format( ' ' * ident, cmap[status], skey, data[status][key], color['ENDC']) return ret