Praca z MS SQL z Powershell na Linuxie

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?

Praca z MS SQL z Powershell na Linuxie
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
EXIT

Jeś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. jest problematyczne.
  • I przede wszystkim: najprawdopodobniej w twoim Linuxie tego cmdletu nie ma!

I to jest główny problem. Dopiero w marcu ten cmdlet stał się dostępny na platformach innych niż Windows,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 $cmd

W 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
  return

W 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

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster