Ten artykuł ma charakter czysto praktyczny i poświęcony jest mojej smutnej historii.
Przygotowując się do Zero Touch PROD dla RDS (MS SQL), o którym mówiono nam bez końca, zrobiłem prezentację (POC — Proof Of Concept) automatyzacji: zestaw skryptów PowerShell. Po prezentacji, gdy ustały burzliwe, długie brawa, przechodzące w nieustające owacje, powiedziano mi — to wszystko jest w porządku, ale z ideologicznych powodów wszyscy nasi Jenkins slaves działają na Linuxie!
Czy to naprawdę możliwe? Wziąć takiego sympatycznego, ciepłego DBA z Windows i wrzucić go prosto w piekło PowerShell pod Linuxem? Czy to nie jest okrutne?

Musiałem zagłębić się w tę dziwną kombinację technologii. Oczywiście, wszystkie moje 30+ skryptów przestały działać. Ku mojemu zaskoczeniu, udało mi się to naprawić w ciągu jednego dnia roboczego. Piszę na gorąco. Więc jakie pułapki mogą na was czekać podczas przenoszenia skryptów PowerShell z Windows na Linux?
sqlcmd vs Invoke-SqlCmd
Przypomnę główną różnicę między nimi. Stara, dobra utilita sqlcmd działa również na Linuxie, z prawie identyczną funkcjonalnością. Zapytanie do wykonania podajemy przez -Q, plik wejściowy jako -i, a wyjście -o. Tylko, że oczywiście nazwy plików są case-sensitive. Jeśli używacie -i, to w pliku napiszcie na końcu:
GO
EXITJeśli na końcu nie ma EXIT, to sqlcmd przejdzie do oczekiwania na wejście, a jeśli przed EXIT nie będzie GO, ostatnia komenda nie zostanie wykonana. Do pliku wyjścia trafia cały wynik, selects, komunikaty, print itd.
Invoke-SqlCmd zwraca wynik w postaci DataSet, DataTables lub DataRows. Dlatego, jeśli możecie przetworzyć wynik prostego select poprzez sqlcmd, analizując jego wyjście, to wypisanie czegoś bardziej złożonego jest praktycznie niemożliwe: do tego służy Invoke-SqlCmd.Ale ta komenda ma też swoje smaczki:
- Jeśli przekazujecie jej plik przez -InputFile, to EXIT nie jest potrzebny, wręcz przeciwnie, zwraca błąd składni.
- -OutputFile nie, polecenie zwraca wam wynik w postaci obiektu.
- Aby określić serwer, istnieją dwa składnie: -ServerInstance -Username -Password -Database i przez -ConnectionString.Jak nie dziwne, w pierwszym przypadku nie można podać portu różnego od 1433.
- Wyjście tekstowe, takie jak PRINT, które łatwo da się „złapać”, sqlcmd, dla Invoke-SqlCmd.
- I przede wszystkim:
I to jest główny problem. Dopiero w marcu ten cmdlet i w końcu możemy iść do przodu!
Podstawianie zmiennych.
W sqlcmd istnieje możliwość podstawienia zmiennych za pomocą -v, na przykład w ten sposób:
# $conn содержит начало команды sqlcmd
$cmd = $conn + " -i D:appsSlaveJobsKillSpid.sql -o killspid.res
-v spid =`"" + $spid + "`" -v age =`"" + $age + "`""
Invoke-Expression $cmdW skrypcie SQL używamy podstawień:
set @spid=$(spid)
set @age=$(age)Tak więc. W *nix podstawienia zmiennych nie działają. Parametr -v jest ignorowane. U Invoke-SqlCmd. jest ignorowany -Zmiennych. Chociaż parametr, który definiuje same zmienne, jest ignorowany, same podstawienia działają — można używać dowolnych zmiennych z Shell. Jednak obraziłem się na zmienne i postanowiłem w ogóle od nich nie zależeć, a zastosowałem brutalne i proste podejście, skoro skrypty SQL są krótkie:
# prepend the parameters
"declare @age int, @spid int" | Add-Content "q.sql"
"set @spid=" + $spid | Add-Content "q.sql"
"set @age=" + $age | Add-Content "q.sql"
foreach ($line in Get-Content "Sqlserver/Automation/KillSpid.sql") {
$line | Add-Content "q.sql"
}
$cmd = "/opt/mssql-tools/bin/" + $conn + " -i q.sql -o res.log"To, jak zrozumieliście, test już z wersji uniksowej.
Ładowanie plików
W wersji Windows każda operacja była związana z audytem: wykonaliśmy sqlcmd, otrzymaliśmy jakiś błąd w pliku wyjściowym, dołączyliśmy ten plik do tabeli audytu. Na szczęście SQL Server działał na tym samym serwerze co Jenkins, robiło się to mniej więcej tak:
CREATE procedure AuditUpload
@id int, @filename varchar(256)
as
set nocount on
declare @sql varchar(max)
CREATE TABLE #multi (filer NVARCHAR(MAX))
set @sql='BULK INSERT #multi FROM '''+@filename
+''' WITH (ROWTERMINATOR = '' '',CODEPAGE = ''ACP'')'
exec (@sql)
select @sql=filer from #multi
update JenkinsAudit set multiliner=@sql where ID=@id
returnW ten sposób wczytujemy plik BCP w całości i wkładamy go do pola nvarchar(max) tabeli audytu. Oczywiście, cały ten system się rozpadł, ponieważ zamiast SQL Servera otrzymałem RDS, a BULK INSERT w ogóle nie działa przez UNC z powodu próby uzyskania wyłącznego dostępu do pliku, a przy RDS to było z góry skazane na niepowodzenie. Postanowiłem więc zmienić projekt systemu, przechowując audyt linia po linii:
CREATE TABLE AuditOut (
ID int NULL,
TextLine nvarchar(max) NULL,
n int IDENTITY(1,1) PRIMARY KEY
)I pisać do tej tabeli w ten sposób:
function WriteAudit([string]$Filename, [string]$ConnStr,
[string]$Tabname, [string]$Jobname)
{
# pobierz $lastid ostatniego wykonania -- pominięto dla artykułu
# stwórz grid i napełnij go danymi z pliku
$audit = Get-Content $Filename
$DT = new-object Data.DataTable
$COL1 = new-object Data.DataColumn;
$COL1.ColumnName = "ID";
$COL1.DataType = [System.Type]::GetType("System.Int32")
$COL2 = new-object Data.DataColumn;
$COL2.ColumnName = "TextLine";
$COL2.DataType = [System.Type]::GetType("System.String")
$DT.Columns.Add($COL1)
$DT.Columns.Add($COL2)
foreach ($line in $audit)
{
$DR = $dt.NewRow()
$DR.Item("ID") = $lastid
$DR.Item("TextLine") = $line
$DT.Rows.Add($DR)
}
# zapisz to do tabeli
$conn=new-object System.Data.SqlClient.SQLConnection
$conn.ConnectionString = $ConnStr
$conn.Open()
$bulkCopy = new-object ("Data.SqlClient.SqlBulkCopy") $ConnStr
$bulkCopy.DestinationTableName = $Tabname
$bulkCopy.BatchSize = 50000
$bulkCopy.BulkCopyTimeout = 0
$bulkCopy.WriteToServer($DT)
$conn.Close()
}
Aby wybrać zawartość, należy wykonać zapytanie select po ID, wybierając w porządku n (identity).
W następnym artykule przyjrzymy się dokładniej, jak to wszystko współdziała z Jenkins.
Źródło: habr.com
