Продолжаю выполнение чужих просьб
В этот раз попросили сделать следующее. Есть файловый сервер и на нем бордак с правами и наследование отсутствует. Нужно найти все папки где в пермишенах нету определенной группы/пользователя. Во время написания скрипта обнаружилось две проблемы. Первая заключалась в том, что нашлись папки куда у администратора вообще доступа не было. Так же нашлись папки с путем больше 256 символов. По этому был добавлен обработчик ошибок который проблемные папки выводит в два файлика для дальнейшего разбирательства. Вот собственно и сама функция:
Function Get-NotPermissions ($path,$group,$error_rights,$error_long) {
#--- очищаем лог ошибок
$error.Clear |out-null
#--- рекурсивно перебираем все папки
foreach ($item in Get-ChildItem -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable error_mass | Where-Object {$_.PSIsContainer}) {
#--- создаем массив с разрешениями
$groups = ($item.PSPath |get-acl).Access | select -expandproperty IdentityReference
#--- проверяем есть ли в массиве интересующая нас группа
if ($groups -notcontains $group) { '"'+$item.fullname+'"'}
}
#--- проверяем существует ли файл и если чего удаляем его
if (Test-Path $error_rights) {Remove-Item -Path $error_rights -Force -Confirm}
if (Test-Path $error_long) {Remove-Item -Path $error_long -Force -Confirm}
#--- обрабатываем полученные ошибки
foreach ($item in $error_mass ) {
#--- нету доступа
if ($item.CategoryInfo.Reason -eq 'UnauthorizedAccessException') {
'"' + $item.TargetObject + '"' |Out-File -Encoding 'Unicode' -FilePath $error_rights -Append
}
#--- длинный путь, больше 256 символов
elseif ($item.CategoryInfo.Reason -eq 'PathTooLongException') {
'"' + $item.TargetObject + '"' |Out-File -Encoding 'Unicode' -FilePath $error_long -Append
}
}
}
Ну и конечно пример запуска:
Get-NotPermissions -path '\\titan\C$\Bin\tmp' -group 'isea\abigor' -error_rights 'D:\error_NTFS.txt' -error_long 'D:\error_long_NTFS.txt'
Попросили у меня совета как можно найти в папке созданные 10 дней назад файлы и удалить их. Подумав у меня родилась вот такая простенькая функция:
Function Search-OldFiles {
param ($day,$path,[switch]$search)
$help = @"
`n
`t Надо использовать следующий синтаксис функции:
`t -day - задаем кол-во дней
`t -path - указываем путь
`t -search - указываем, что необходимо только найти файлы (по умолчанию они удаляются)
`t------------------------------------------------------------------------------------
`t Пример:
`t Search-OldFiles -day 2 -path 'D:\Games' -search
"@
#--- проверяем существование параметров функции
if (!($day) -or !($path) ) {write-warning $help; return}
#--- рукурсивно просматриваем папки и выбираем файлы
foreach ($item in Get-ChildItem -LiteralPath $path -Recurse -Force | Where-Object {!$_.PSIsContainer}) {
#--- сравниваем дату создания файла
if ((Get-Date).adddays(-$day) -ge $item.CreationTime) {
#--- в зависимости от ключа search просто ищем или удаляем
if ($search) { '"' + $item.fullname + '"' }
else { $item.fullname | Remove-Item -Force -WhatIf }
}
}
}
Начнем с MySQL. С ним оказалось не все так просто. Для работы с ним необходимо на компьютер где будет работать функция/скрипт установить Connector/Net от MySQL соответствующий версии вашего сервера. Прочитав доку в общем получается одна функция для select/inserd/delete/update.
Function Connect-ToMySQL {
param([String]$query,[String]$database,[String]$hostmysql,[String]$login,[String]$password)
#--- загрудаем компоненты mysql
[void][system.reflection.Assembly]::LoadWithPartialName("MySql.Data")
#--- открываем подключение
$connStr = "server=$hostmysql;port=3306;uid=$login;pwd=$password;database=$database;Pooling=False"
$conn = New-Object MySql.Data.MySqlClient.MySqlConnection($connStr)
$conn.Open()
#--- создаем объекты для выполнения запроса
$cmd = New-Object MySql.Data.MySqlClient.MySqlCommand($query, $conn)
$da = New-Object MySql.Data.MySqlClient.MySqlDataAdapter($cmd)
$ds = New-Object System.Data.DataSet
#--- получение запроса
$da.Fill($ds) | Out-Null
#--- ну и вывод на экран
$ds.Tables[0]
#--- закрываем подключени
$conn.close()
}
В итоге получаем вот такой результат:
[PS] <16> C:\Bin>$inquiry = 'SELECT `domain-id`,username,active FROM users WHERE username="abigor@isea.ru"'
[PS] <17> C:\Bin>Connect-ToMySQL -database 'mail-server' -login 'usertest' -password 'pass' -hostmysql 'sqlsrv' -query $inquiry
domain-id username active
--------- -------- ------
1 abigor@isea.ru true
[PS] <18> C:\Bin>
[PS] <18> C:\Bin>$inquiry = 'update users set active="false" WHERE username="abigor@isea.ru"'
[PS] <19> C:\Bin>Connect-ToMySQL -database 'mail-server' -login 'usertest' -password 'pass' -hostmysql 'sqlsrv' -query $inquiry
[PS] <20> C:\Bin>$inquiry = 'SELECT `domain-id`,username,active FROM users WHERE username="abigor@isea.ru"'
[PS] <21> C:\Bin>Connect-ToMySQL -database 'mail-server' -login 'usertest' -password 'pass' -hostmysql 'sqlsrv' -query $inquiry
domain-id username active
--------- -------- ------
1 abigor@isea.ru false
[PS] <22> C:\Bin>
Все бы хорошо. но есть одна проблема. Если у вас запрос в стоит в апострофе, то экранировать имена полей с дефисом надо одинарными обратным апострофом
. Если же в запрос взят в кавычки, то экранировать надо двумя обратными апострофами
. Понятно почему так получается. Из-за того, что все кавычки позволяют раскрыть содержимое, а все что в апострофах воспринимается как текст.
Был у нас когда-то совсем давно сервер с именем MAAT и с ролью web сервера. Время шло и мы его убили. В место него появились два сервера IIS-01 и IIS-02 с теми же ролями. Так же для пользователей, которые используют особо важные программы отдела АСУ был ограничен вход на компьютеры. Ну и как говорится началась глупая работа по замене старого сервера на новые. После третьего звонка я не выдержал и набросал вот такой скрипт. Который ищет в домене пользователей у которых в свойстве userWorkstations есть компьютер MAAT и заменяет его на нужные значения.
#Requires -Version 2.0
#Requires -PSSnapin Quest.ActiveRoles.ADManagement
#########################################################################################
# Created: Zakharchenko Andrey Ruslanovich
# Date: 2009.09.16
# Description: Скрипт ищет всех пользователей с определенным компьютером в свойстве
# userworkstations и заменяет их нужными именами компьютеров
#########################################################################################
Function Search-MAAT ($listcomputers) {
#--- создаем пустой масив
$mass = New-Object System.Collections.ArrayList
#--- заполняем массив
Foreach ($i in $listcomputers -split ',') { $mass.Add($i) | Out-Null }
$i = 0
While ($i -ne $mass.Count+1) {
#--- ищем совпадения и устанвливаем флаг
if (($mass[$i] -match 'maat') -and ($listcomputers -match 'IIS-01') `
-and ($listcomputers -match 'IIS-02')) {
#--- тут флага нету, так как мы сразу удаяем не нежный элемент массива
$mass.Removeat($i)
}
if (($mass[$i] -match 'maat') -and ($listcomputers -match 'IIS-01')) {
$check = 'iis-02'
}
if (($mass[$i] -match 'maat') -and ($listcomputers -match 'IIS-02')) {
$check = 'iis-01'
}
if (($mass[$i] -match 'maat') -and ($listcomputers -notmatch 'IIS-01') `
-and ($listcomputers -notmatch 'IIS-02')) {
$check = 'all'
}
#--- обрабатываемт полученные флаги
switch ($check) {
'all' { $mass[$i] = 'IIS-01,IIS-02' }
'iis-01' { $mass[$i] = 'IIS-01' }
'iis-02' { $mass[$i] = 'IIS-02' }
default { }
}
#--- удаляем при каждом прохоте на всякий случай переменную с флагом
if($check) {Remove-Variable check}
$i++
}
#--- собираем массив в строку с разделителем ','
$mass -join ','
}
$root = 'DC=isea,DC=ru'
Foreach ($user in Get-QADUser * -IncludedProperties 'userWorkstations' -SizeLimit 0 -SearchRoot $root) {
if ( $user.userWorkstations -match 'maat' ) {
Write-Host "Работы проводится над следующим пользователем: $($user.DisplayName)"
Set-QADUser $user -ObjectAttributes @{'userworkstations'=$(Search-MAAT $user.userworkstations)} |Out-Null
}
}
Собственно лень двигатель прогресса
Имена компьютеров можно изменить и использовать скрипт повторно.
В продолжение прошлого поста публикую функцию отображения где какие роли находятся. Я понимаю что подобных скриптов уже полно. НО как говорится хотелось сделать самому, да и разобраться с этим делом. А не использовать безвольно уже готовое.
#########################################################################################
# Created: Zakharchenko Andrey Ruslanovich
# Date: 2009.09.07
# Description: Функция получения ролей хранящихся на контроллерах домена
#########################################################################################
Function Get-DomainRoles {
param($domain)
#--- если домен не задан получает текущий домен
if (!$domain) {
$domain = (Get-WmiObject -ComputerName '.' -Query 'Select domain From Win32_ComputerSystem').Domain
}
#--- подключаемся к указанному домену и осуществляем получение информации
$context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext(”Domain”,$domain)
$dclist = [System.DirectoryServices.ActiveDirectory.DomainController]::findall($context)
#--- разбираем ролученные данные
Foreach ( $chek in $dclist ) {
#--- отфильтровывает КД на которых нету ролей
if ($chek.Roles -ne "" ) {
$i = 0
#--- ну и самое вкусное
while ($i -ne $chek.Roles.Count) {
switch ($chek.Roles[$i]) {
'SchemaRole' { $role = 'Хозяин схемы' }
'NamingRole' { $role = 'Хозяин именования домена' }
'PdcRole' { $role = 'Эмуляция главного контроллера домена (PDC)' }
'RidRole' { $role = 'Хозяин относительных идентификаторов (RID)' }
'InfrastructureRole' { $role = 'Хранитель инфраструктуры' }
}
$tmp = "" | Select @{n='Roles';e={$($role)}},@{n='DC';e={$($chek.name.ToLower())}}
$tmp
$i++
}
}
}
}
Ну и собственно пример работы:
[PS] <7> C:\Bin>Get-DomainRoles
Roles DC
----- --
Эмуляция главного контроллера домена (PDC) dc-01.isea.ru
Хозяин относительных идентификаторов (RID) dc-01.isea.ru
Хозяин схемы dc-02.isea.ru
Хозяин именования домена dc-02.isea.ru
Хранитель инфраструктуры dc-02.isea.ru
[PS] <8> C:\Bin>
[PS] <8> C:\Bin>Get-DomainRoles -domain "student.isea.ru"
Roles DC
----- --
Эмуляция главного контроллера домена (PDC) dc-03.student.isea.ru
Хозяин относительных идентификаторов (RID) dc-03.student.isea.ru
Хранитель инфраструктуры dc-03.student.isea.ru
[PS] <9> C:\Bin>
Первая функция получает необходимые свойства объекта из Active Directory
#########################################################################################
# Created: Zakharchenko Andrey Ruslanovich
# Date: 2009.09.07
# Description: Функция получения свойства объект[а|ов] из Active Directory
#########################################################################################
Function Get-ADProperty {
param ($name, $type, $property, $domain)
#--- если не задан домен, то получаем его самостоятельно
if (!$domain) {
$domain = ([adsi]"").DistinguishedName
}
#--- собственно сам фильтр
$strFilter = "(&(objectCategory=$type)(cn=$name))"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain")
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
#--- обозначаем корень поиска
$objSearcher.SearchRoot = $objDomain
#--- чтоб случайно не задосить Active Directory
$objSearcher.PageSize = 1000
#--- применяем фильтр
$objSearcher.Filter = $strFilter
#--- ищем
$colResults = $objSearcher.FindAll()
#--- выводим результат
Foreach ($objResult in $colResults) {
$objResult.Properties.$($property.tolower())
}
Пример запуска и получаемого результата:
[PS] <25> C:\Bin>Get-ADProperty -type "user" -name "Захарченко Андрей" -property "homeDirectory"
\\balance.isea.ru\homes$\abigor
[PS] <26> C:\Bin>Get-ADProperty -type "group" -name "пользовате*" -property "DistinguishedName" -domain "DC=student,DC=isea,DC=ru"
CN=Пользователи,CN=Builtin,DC=student,DC=isea,DC=ru
CN=Пользователи DCOM,CN=Builtin,DC=student,DC=isea,DC=ru
CN=Пользователи домена,CN=Users,DC=student,DC=isea,DC=ru
CN=Пользователи журналов производительности,CN=Builtin,DC=student,DC=isea,DC=ru
CN=Пользователи системного монитора,CN=Builtin,DC=student,DC=isea,DC=ru
CN=Пользователи удаленного рабочего стола,CN=Builtin,DC=student,DC=isea,DC=ru
[PS] <27> C:\Bin>
Следующая функция получает название текущего домена в формате domain.com
Function Get-Domain {
(Get-WmiObject -ComputerName '.' -Query 'Select domain From Win32_ComputerSystem').Domain
}
Понадобилось мне приводить название подразделений к стандартизованному виду. Который был заложен еще до меня. В базе по сотрудникам их подразделения написаны все в верхнем регистре. Что согласитесь в Active Directory это бы смотрелось не очень хорошо. По этому у меня появилась необходимость автоматизировать этот процесс. В ходе чего на свет появилась такая функция:
#########################################################################################
# Created: Zakharchenko Andrey Ruslanovich
# Date: 2009.07.17
# Description: Функция смена регистра подразделений/текста.
# Приведение его к общему стандарту.
#########################################################################################
Function Replace-Register ($text) {
#--- в цикле разрезаем текст на слова
Foreach ($var in ($text).Split(' ')) {
#--- проверяем если слово состоит из одного символа, делаем нижний регистр
if ($var.Length -eq 1) {
$j = $var.ToLower()
}
#--- если слово разделено "-" то отдельно с ним работаем
elseif ($var -cmatch '-') {
#--- переключатель
$switch = 0
#--- разделяем слово по "-"
Foreach ( $var1 in $var.Split('-')) {
#--- если это первое слово, то в конец добавляем "-"
if ($switch -eq 0 ) {
$j1 = $(($var1.Substring(0,1)).ToUpper()) + $(($var1.Substring(1)).ToLower()) + '-'
}
else {
$j1 = $(($var1.Substring(0,1)).ToUpper()) + $(($var1.Substring(1)).ToLower())
}
#--- собираем в строку
$j = $j + $j1
$switch++
}
}
#--- работа над обычными словами не подошедшими не под один фильтр
else {
$j = $(($var.Substring(0,1)).ToUpper()) + $(($var.Substring(1)).ToLower())
}
#--- собираем текст в строку
$textuot = $textuot + ' ' + $j
Remove-Variable j
}
#--- выводим результат
$textuot.Trim()
}
Ну и проверим чего у нас получилось:
[PS] <42> C:\>Replace-Register 'ИРКУТСКИЙ И БРАТСКИЙ ТОРГОВО-ЭКОНОМИЧЕСКИЕ КОЛЛЕДЖИ'
Иркутский и Братский Торгово-Экономические Колледжи
[PS] <43> C:\>Replace-Register 'иркутский и братский торгово-экономические колледжи'
Иркутский и Братский Торгово-Экономические Колледжи
[PS] <44> C:\>
Наш вуз стал участником программы от Microsoft под название Live@Edu. Это нечто бесплатной почтовой системы с ящиком в 25Gb и в дальнейшем с тесной интеграцией с продуктами Microsoft. При этом в систему можно загрузить необходимых пользователей через csv файл и тем самым завести всех кому необходима данная система. Пока был написан простенький скрипт для генерации csv файла с необходимыми данными внутри и для последующей его отсылки человеку который нас курирует. Так как до 2009.07.01 необходимо было завести не меньше 500 учеток для подтверждения внедрения данного продукта в организации. По этому скрипт очень простой. В дальнейшем он будет дописан и расширен функционал.
#Requires -PSSnapin Quest.ActiveRoles.ADManagement
#########################################################################################
# Created: Zakharchenko Andrey Ruslanovich
# Date: 2009.06.28
# Description: Экспорт сотрудников в CSV для создания почтовых ящиков в систему Live@Edu
#########################################################################################
#--- функция генерации пароля
Function New-Password ([int]$intPasswordLength){
#--- проверяем что пароль больше 4 символов
if ($intPasswordLength -lt 4) {return "password cannot be <4 chars"}
#--- наборы символов для генерации
$strNumbers = "1234567890"
$strCapitalLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
$strLowerLetters = "abcdefghjkmnpqrstuvwxyz"
$strSymbols = "!%^&*()+=/?{}[]~,.<>:"
#--- собственно сама генерация пароля
$rand = new-object random
for ($a=1; $a -le $intPasswordLength; $a++){
if ($a -gt 3){
$b = $rand.next(0,4) + $a
$b = $b % 3 + 1}
else { $b = $a }
switch ($b){
"1" {$b = "$strNumbers"}
"2" {$b = "$strCapitalLetters"}
"3" {$b = "$strLowerLetters"}
"4" {$b = "$strSymbols"}}
$charset = $($b)
$number = $rand.next(0,$charset.Length)
$RandomPassword += $charset[$number]
}
return $RandomPassword
}
#--- объявляем переменные
$SQLServer = "SQLSERVER"
$DBName = "DB_SOTRUDNIC"
$i = 0
$domain = 'bguep.info'
#--- создаем подключение к Microsoft SQL Server
$SQLConnection = new-object System.Data.SqlClient.SqlConnection("Initial Catalog=$DBName;Data Source=$SQLServer;Integrated Security=SSPI")
$SQLConnection.Open()
#--- основной цикл выполнения
ForEach ($user in Get-QADUser -searchroot "OU=БГУЭП,DC=isea,DC=ru" -IncludedProperties WebPage,LogonName,AccountIsDisabled -SizeLimit 0 ) {
#ForEach ($user in Get-QADUser -searchroot "OU=Информационное Управление,OU=БГУЭП,DC=isea,DC=ru" -IncludedProperties WebPage,LogonName,AccountIsDisabled -SizeLimit 0 ) {
if ($user.webpage){
if (!($user.webpage -match "^Q")) {
#--- ищем, что пользователь является сотрудником вуза (проверяем по "рег. номеру")
$query = "SELECT TOP (1) регном FROM dbo.BankCustomer WHERE регном = " + $($user.WebPage -replace "[^0-9 \d]") + ""
$SQLCommand = New-Object System.Data.SqlClient.SqlCommand($query, $SQLConnection)
$SQLReader = $SQLCommand.ExecuteScalar()
#--- вытаскиваем должность сотрудника
$query = "SELECT TOP (1) Должность FROM dbo.Customer WHERE регном= " + $($user.WebPage -replace "[^0-9 \d]") + ""
#--- проверяем, что "рег. номер" не пустой и пользователь не отключен в AD
if ($SQLReader -and !$user.AccountIsDisabled) {
#--- создаем объект для дальнейщего экспорта
$export = "" | Select-Object @{name='EmployeeID';expression={$user.webpage}}, `
@{name='FullName';expression={$user.DisplayName}}, `
# @{name='Post';expression={(New-Object System.Data.SqlClient.SqlCommand(("SELECT TOP (1) Должность FROM dbo.Customer WHERE регном= " + $($user.WebPage -replace "[^0-9 \d]") + ""), $SQLConnection)).ExecuteScalar()}}, `
@{name='Email';expression={(($user.LogonName).ToLower() + "@$domain")}}, `
@{name='Password';expression={(New-Password (10))}}
$export
$i++
}
}
}
}
Write-Output "Итого пользователей: $($i)"
$SQLConnection.Close()
Вот таким коротким кодом в одну строку сделал импорт всей резервации.
ForEach ($address in Get-Content dhcp-export.txt) { $tmp = $address.split("`t"); if ($tmp[0] -notmatch "^10\.1\.2\.") {`
netsh dhcp server \\10.1.1.39 scope 10.1.0.0 add reservedip $tmp[0] $tmp[4] $tmp[1]| Out-Null }}
Тут конечно нету ни чего героического. Разместил так, для себя чтоб не забыть.
У меня в организации есть база данных в которой отображается вся миграция сотрудников внутри вуза. Из этой базы мы когда заводим сотруднику учетную запись в Active Directory в поле WebPage заносим ID пользователя. Так, что вот, родился скрипт чтоб вычистить всех уволенных пользователей. Пока он просто на консоль выводит информацию, чтоб инженер мог вручную все просмотреть и принять решение, но его легко можно модифицировать чтоб он сам блокировал/удалял учетную запись пользователя.
param ([switch]$show, [switch]$disable, [switch]$csv)
#########################################################################################
# Created: Zakharchenko Andrey Ruslanovich
# Date: 2009.04.29
# Description: Поиск уволенных пользователей в Active Directory через запрос в
# к MS SQL серверу
# Dependencies: Quest.ActiveRoles.ADManagement
#########################################################################################
$SQLServer = "SQLSERVER"
$DBName = "DB_SOTRUDNIC"
$i = 0
$text = "`n
`t Надо использовать следующий синтаксис скрипта:
`t -show - Выводит информацию на консоль
`t -disable - Отключает всех уволенных сотрудников
`t -csv - Выводит информацию в формате для csv
`t------------------------------------------------------------------------------------
`t Пример:
`t ./search-delete-user-from-ad.ps1 -show "
if (!($show) -and !($disable) -and !($csv)) {write-warning $text; return}
$SQLConnection = new-object System.Data.SqlClient.SqlConnection("Initial Catalog=$DBName;Data Source=$SQLServer; `
Integrated Security=SSPI")
$SQLConnection.Open()
ForEach ($user in Get-QADUser -searchroot "OU=БГУЭП,DC=isea,DC=ru" -IncludedProperties WebPage,LastLogon,ParentContainer, `
whenchanged,LogonName,AccountIsDisabled -SizeLimit 0 ) {
if ($user.webpage){
if (!($user.webpage -match "^Q")) {
$query = "SELECT TOP (1) регном FROM dbo.Customer WHERE регном = " + $user.WebPage + ""
$SQLCommand = New-Object System.Data.SqlClient.SqlCommand($query, $SQLConnection)
$SQLReader = $SQLCommand.ExecuteScalar()
if (!$SQLReader) {
if ($show) {
Write-Output "Name: $($user.DisplayName)"
Write-Output "LogonName: $($user.LogonName)"
Write-Output "WebPage: $($user.WebPage)"
Write-Output "AccountIsDisabled: $($user.AccountIsDisabled)"
Write-Output "ParentContainer: $($user.ParentContainer)"
Write-Output "Whenchanged: $($user.whenchanged)"
Write-Output "LastLogon: $($user.LastLogon)"
Write-Output "-----------------------------------------------------------------------"
}
if ($disable) {
If ($user.AccountIsDisabled -eq $false) {
($user).AccountIsDisabled = $true
($user).CommitChanges()
Write-Output "Name: $($user.DisplayName) - Отключен"
Write-Output "AccountIsDisabled: $($user.AccountIsDisabled)"
}
}
if ($csv) {
$tmp = "" |Select-Object @{name='Name';expression={$($user.DisplayName)}},@{name='LogonName';expression={$($user.LogonName)}}, `
@{name='WebPage';expression={$($user.WebPage)}},@{name='AccountIsDisabled';expression={$($user.AccountIsDisabled)}}, `
@{name='ParentContainer';expression={$($user.ParentContainer)}},@{name='Whenchanged';expression={$($user.whenchanged)}}, `
@{name='LastLogon';expression={$($user.LastLogon)}}
Write-Output $tmp
}
$i++
}
}
}
}
Write-Output "Итого пользователей: $($i)"
# Out-file -filepath "Пользователи к удалению.txt" -Encoding "Unicode"
$SQLConnection.Close()