Własna kontrolka SSIS, część 6 – podłączamy się do serwera SFTP

Mamy szkielet kontrolki. Umiemy napisać coś, co się poprawnie kompiluje i nawet pojawia się w przyborniku SSIS (SSIS Toolbox). Sukces! To teraz pora sprawić, żeby kontrolka mogła się połączyć z serwerami SFTP i SQL. Do roboty.

Na pierwszy ogień połączymy się z SFTP (dla FTPS będzie podobnie). Jeśli nie mamy jakiegoś serwera pod ręką proponuję ściągnąć darmowy Rebel Tiny SFTP Server, który bardzo dobrze się sprawdza do testowania własnego kodu. Po uruchomieniu automatycznie generują się potrzebne klucze i serwer jest gotowy do działania.

RebelTinySFTPServer

Potrzebujemy kilku elementów do połączenia z serwerem. Standardowo niezbędne są: adres serwera, nazwa użytkownika i hasło. Oprócz tego możemy zmienić protokół i port jeśli są inne od domyślnie przyjętych SFTP i 22. Żeby ustawić te wartości wykorzystujemy właściwości klasy (properties), np.

public string HostName { get; set; }

Każda taka właściwość będzie automatycznie widoczna w Property grid w ramach sekcji „Misc”. Jeśli chcielibyśmy mieć własną sekcję (pewnie że chcemy!) wykorzystujemy atrybut Category:

[Category("WinSCP Session Options")]
[Description("Name of the host to connect to. Mandatory property.")]
public string HostName { get; set; }

PropertyGridSessionOptions

W przykładzie powyżej pojawia się też Description – czyli opis, który zobaczymy po wybraniu opcji w siatce właściwości (nadal nie mogę się przekonać do polskich tłumaczeń). Atrybuty znajdziemy w przestrzeni nazw System.ComponentModel. Dla haseł dodajemy dodatkowy atrybut: [PasswordPropertyText(true)], dzięki któremu będzie wyświetlane zamaskowane.

WinSCP potrzebuje też znajomości jeszcze przynajmniej jednego parametru, a w przypadku SFTP lub FTPS nawet dwóch. Są to: ścieżka do pliku winscp.exe (jeśli jest inna niż ścieżka do WinSCPNet.dll) oraz „odcisk palca” serwera (fingerprint). Jeśli ich nie podamy, to pojawią się błędy i  nie będzie działać.

WinSCP.exe.Missing

SshHostKeyFingerprint.Missing

Na pierwszym obrazku pojawia się informacja, że nie znaleziono winscp.exe w lokalizacji tej samej co assembly WinSCPNet i pojawia się ścieżka do Global Assembly Cache. Czemu tak? W ramach ustawiania właściwości posługujemy się dwoma typami danych z biblioteki WinSCP: Protocol oraz FtpSecure. Żeby ich obsługa była możliwa biblioteka musi być w miejscu znanym przez system. Dla uproszczenia programowania zarejestrowałem WinSCPNet w GAC analogicznie jak przy rejestracji własnej kontrolki, a rozpoznanie innych możliwości zostawiłem na kiedy indziej.

Drugi obrazek to komunikat o braku SshHostKeyFingerprint. Możemy go otrzymać łącząc się z serwerem po raz pierwszy za pomocą winscp.exe i kopiując go do schowka. Jeśli już kilka razy łączyliśmy się do serwera, to (również za pomocą winscp.exe) otwieramy sesję i w menu wybieramy Session > Server and protocol information i w zakładce Protocol w części Server host key fingerprint znajduje się klucz do skopiowania.

SshHostKeyFingerprint.Get

Teraz ważna decyzja projektowa – budujemy kontrolkę, która ma obsłużyć całą komunikację z serwerami, sprawdzić co znajduje się na serwerze i pobrać interesujące nas pliki. Zamiast tworzyć jedną czarną skrzynkę rozmontujmy to na kilka elementów. Nie dość, że będziemy bardziej pro i nie będziemy się bać użyć słów dekompozycja, czy rozdzielenie odpowiedzialności, to przy okazji nauczmy się czegoś nowego. Czyli pora na (werble!)

Custom Connection Manager

Brzmi poważnie, ale connection manager (manager połączeń) w SSIS to po prostu pojemnik na zmienne potrzebne do nawiązania połączenia. Do źródła dobija się i tak task lub component, w którym ten connection manager zostanie użyty.

Zadanie zaczynamy od zdefiniowania odrębnego projektu – będziemy budować oddzielne assembly. Tworzymy nową bibliotekę, w której klasa będzie dziedziczyć po ConnectionManagerBase i posiadać atrybut DtsConnection.

[DtsConnection(ConnectionType = "WINSCP", DisplayName = "WinSCP Connection Manager", Description = "Connection manager for SFTP/FTPS/FTP using WinSCPNet.dll library", ConnectionContact = "BartekR, bartekr.net")]
public class WinSCPConnectionManager : ConnectionManagerBase
{
    // code
}

Do tego definiujemy properties dla każdego elementu, który będziemy chcieli ustawić w ramach property grid i przeciążamy dwie metody: AcquireConnection() (nawiąż połączenie) i ReleaseConnection() (zwolnij połączenie).

W ramach AcquireConnection() ustawiamy opcje sesji połączenia WinSCP, ścieżkę do winscp.exe i poprzez metodę Open() nawiązujemy połączenie z serwerem. Ponieważ metoda musi zwracać obiekt, a metoda Open() w WinSCPNet jest typu void, to zwracamy to, co domyślnie zwraca klasa bazowa. Póki co nas to nie boli.

W ReleaseConnection() wystarczy zamknąć połączenie metodą Close(). Do kompletu dorzucimy sprawdzanie poprawności – metodę Validate(), która zweryfikuje czy wprowadzono adres, użytkownika i hasło. Bardziej, żeby pokazać że można i jak to zrobić niż z powodu większej przydatności – przynajmniej na początku.

public override object AcquireConnection(object txn)
{
    SessionOptions so = new SessionOptions
    {
        HostName = this.HostName,
        UserName = this.UserName,
        Password = this.Password,
        SshHostKeyFingerprint = this.SshHostKeyFingerprint
    };

    this.s.ExecutablePath = this.ExecutablePath;

    this.s.Open(so);

    return base.AcquireConnection(txn);
}

public override void ReleaseConnection(object connection)
{
    this.s.Close();
}

public override DTSExecResult Validate(IDTSInfoEvents infoEvents)
{
    if (string.IsNullOrEmpty(this.HostName) || string.IsNullOrEmpty(this.UserName) || string.IsNullOrEmpty(this.Password))
    {
        infoEvents.FireError(0, "WinSCP Connection Manager", "Hostname, username and password are mandatory.", string.Empty, 0);
        return DTSExecResult.Failure;
    }
    else
    {
        return DTSExecResult.Success;
    }
}

Na początek tyle wystarczy do utworzenia własnego managera połączeń. Niestety możemy go wykorzystać wyłącznie jako manager na poziomie pakietu, ponieważ tworzenie managera na poziomie projektu wymaga dedykowanego formularza do konfiguracji. W przypadku managera dla pakietu możemy wszystko ustawić z poziomu Property grid. Na potrzeby ułatwienia testowania w konstruktorze klasy ustawiamy jeszcze domyślne parametry połączenia i gotowe.

public WinSCPConnectionManager()
{
    HostName = "localhost";
    UserName = "tester";
    Password = "password";
    Protocol = Protocol.Sftp;
    SshHostKeyFingerprint = "ssh-rsa 2048 52:4b:46:87:88:a2:90:b5:75:ff:49:ff:57:22:72:42";

    ExecutablePath = @"C:\tools\winscp-sdk\winscp.exe";
}

Żeby zbudować i wdrożyć nasze assembly w odpowiednie miejsce przygotowujemy podobny skrypt Post-build jak dla głównego projektu kontrolki. Jedyna różnica to ścieżka docelowa – nasz manager połączeń musi trafić do podkatalogu Connections. Po restarcie Visual Studio będzie można skorzystać z assembly.

cd $(ProjectDir)
@SET CONNDIR="C:\Program Files (x86)\Microsoft SQL Server\130\DTS\Connections\"
@SET GACUTIL="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\gacutil.exe"
Echo Instalacja $(OutDir)$(TargetFileName) w GAC
%GACUTIL% /if "$(OutDir)$(TargetFileName)" /nologo
Echo Kopiowanie "$(OutDir)$(TargetFileName)" do katalogu Connections (%CONNDIR%)
copy "$(OutDir)$(TargetFileName)" %CONNDIR%

Testujemy – klikamy prawym przyciskiem myszy na obszarze „Connection Managers” pakietu SSIS, wybieramy „New Connection…” i (fanfary) nasz Connection manager jest na liście.

CustomConnectionManager

No to teraz

Use the connection, Luke!

Dodajmy do naszej kontrolki informacje, że może już skorzystać z połączenia za pomocą WinSCP, a przy okazji też SQL Server. Utworzymy dwie właściwości: WinSCPConnectionStringSQLServerConnectionString. Przechowamy w nich nazwy managerów połączeń – najprościej będzie skopiować je z właściwości managerów i wkleić do konfiguracji kontrolki w property grid.

Pozostaje wykorzystać nasz własny manager połączeń. Pisaliśmy metodę AcquireConnection() i właśnie ją musimy wywołać. Zrobimy to w ramachExecute().

public override DTSExecResult Validate(Connections connections, VariableDispenser variableDispenser, IDTSComponentEvents componentEvents, IDTSLogging log)
{
    try
    {
        ConnectionManager cmW = connections[this.WinSCPConnectionManagerName];
    }
    catch (System.Exception e)
    {
        componentEvents.FireError(0, "WinSCPTask", "Invalid WinSCP connection manager. " + e.Message, "", 0);
        return DTSExecResult.Failure;
    }

    try
    {
        ConnectionManager cmS = connections[this.SQLServerConnectionManagerName];

    }
    catch (System.Exception e)
    {
        componentEvents.FireError(0, "WinSCPTask", "Invalid SQL Server connection manager. " + e.Message, "", 0);
        return DTSExecResult.Failure;
    }

    return DTSExecResult.Success;

}

Oprócz tego zweryfikujemy za pomocą Validate() czy ustawiono nazwy managerów połączeń dla kontrolki. Jeśli są ustawione oba, wówczas wszystko OK.

public override DTSExecResult Execute(Connections connections, VariableDispenser variableDispenser, IDTSComponentEvents componentEvents, IDTSLogging log, object transaction)
{
    try
    {
        ConnectionManager cmW = connections[this.WinSCPConnectionManagerName];
        object connection = cmW.AcquireConnection(transaction);
    }
    catch (System.Exception e)
    {
        componentEvents.FireError(0, "WinSCPTask - WinSCPConnection", e.Message, "", 0);
        return DTSExecResult.Failure;
    }

    try
    {
        ConnectionManager cmS = connections[this.SQLServerConnectionManagerName];
        object connection = cmS.AcquireConnection(transaction);

    }
    catch (System.Exception e)
    {
        componentEvents.FireError(0, "WinSCPTask - SqlServerConnection", e.Message, "", 0);
        return DTSExecResult.Failure;
    }

    return DTSExecResult.Success;

}

Jeśli wszystko poszło zgodnie z planem to po uruchomieniu pakietu powinniśmy zakończyć dzień nieuchronnym sukcesem. + 100 XP i odznaka „pierwszy connection manager”.

A pełny kod oczywiście na githubie.

Na koniec dwie sprawy, które mogą nam zmącić radość. Pierwsza – brakuje GUI do konfiguracji połączenia, dzięki czemu moglibyśmy go użyć na poziomie projektu, a nie tylko pakietu. Druga – jeśli spojrzymy na kod źródłowy pakietu SSIS zobaczymy, że hasło do serwera nie jest szyfrowane:

<DTS:ConnectionManagers>
    <DTS:ConnectionManager
      DTS:refId="Package.ConnectionManagers[WinSCP Connection Manager]"
      DTS:CreationName="WINSCP"
      DTS:DTSID="{604C40E7-63E6-49E8-B13D-55A800634526}"
      DTS:ObjectName="WinSCP Connection Manager">
      <DTS:ObjectData>
        <InnerObject>
(…)
          <Password
            Type="8"
            Value="password" />
(…)
        </InnerObject>
      </DTS:ObjectData>
    </DTS:ConnectionManager>
</DTS:ConnectionManagers>

Wiemy jednak, że nie są to sprawy, z którymi nie dalibyśmy sobie rady. Żeby jednak nie rozpraszać się zbytnio – wrócimy do tematu GUI i ukrywania hasła za kilka postów. W najbliższym czasie skoncentrujemy się na komunikacji z serwerem SFTP i pobieraniu plików.

Advertisements

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s