
Stworzenie aplikacji do tworzenia kopii zapasowych działającej na dowolnej dystrybucji to niełatwe zadanie. Aby zapewnić działanie Veeam Agent for Linux na dystrybucjach od Red Hat 6 i Debian 6 do OpenSUSE 15.1 i Ubuntu 19.04, trzeba rozwiązać szereg problemów, zwłaszcza biorąc pod uwagę, że w skład produktu wchodzi moduł jądra.
Artykuł powstał na podstawie wystąpienia na konferencji .
Linux to nie tylko jeden z najpopularniejszych systemów operacyjnych. W istocie jest to platforma, na której można stworzyć coś unikalnego, coś własnego. Dzięki temu istnieje wiele dystrybucji Linuxa, które różnią się zestawem komponentów programowych. I tu pojawia się problem: aby produkt programowy działał na każdej dystrybucji, trzeba uwzględnić cechy każdej z nich.
Menadżery pakietów. .deb vs .rpm
Zacznijmy od oczywistego problemu rozprzestrzeniania produktu na różne dystrybucje.
Najbardziej typowy sposób rozprzestrzeniania produktów programowych to umieszczenie pakietu w repozytorium, aby wbudowany w system menadżer pakietów mógł go stamtąd zainstalować.
Jednak popularnych formatów pakietów mamy dwa: rpm i deb. To oznacza, że trzeba wspierać każdy z nich.
W świecie pakietów deb kompatybilność jest zdumiewająca. Ten sam pakiet działa równie dobrze zarówno na Debianie 6, jak i na Ubuntu 19.04. Standardy procesu tworzenia pakietów i pracy z nimi, ustalone w starych dystrybucjach Debiana, pozostają aktualne także w modnych dystrybucjach jak Linux Mint i elementary OS. Dlatego w przypadku Veeam Agent for Linux wystarczy jeden pakiet deb dla każdej platformy sprzętowej.
A w świecie pakietów rpm różnice są ogromne. Po pierwsze, dlatego że są dwaj całkowicie niezależni dystrybutorzy Red Hat i SUSE, dla których wcale nie jest potrzebna kompatybilność. Po drugie, ci dystrybutorzy mają dystrybucje z wsparciem technicznym i eksperymentalne. Między nimi także nie jest potrzebna kompatybilność. Okazuje się, że dla el6, el7 i el8 mamy swoje pakiety. Osobno pakiet dla Fedory. Pakiety dla SLES11 i 12 oraz osobny dla openSUSE. Głównym problemem są zależności i nazwy pakietów.
Problem zależności
Niestety, te same pakiety często występują pod różnymi nazwami w różnych dystrybucjach. Poniżej znajduje się niekompletny wykaz zależności pakietu veeam.
Dla EL7:
Dla SLES 12:
- libblkid
- libgcc
- libstdc++
- ncurses-libs
- fuse-libs
- file-libs
- veeamsnap = 3.0.2.1185
- libblkid1
- libgcc_s1
- libstdc++6
- libmagic1
- libfuse2
- veeamsnap-kmp = 3.0.2.1185
W wyniku tego lista zależności okazuje się unikalna dla dystrybucji.
Gorzej, gdy pod starą nazwą pakietu zaczyna się ukrywać zaktualizowana wersja.
Przykład:
W Fedora 24 zaktualizowano pakiet ncurses z wersji 5 do wersji 6. Nasz produkt był budowany właśnie z wersją 5, aby zapewnić zgodność ze starszymi dystrybucjami. Aby skorzystać ze starej wersji 5 biblioteki w Fedora 24, trzeba było użyć pakietu ncurses-compat-libs.
W wyniku tego dla Fedory pojawiają się dwa pakiety, z różnymi zależnościami.
Potem jest ciekawiej. Po kolejnej aktualizacji dystrybucji pakiet ncurses-compat-libs z wersją 5 biblioteki okazuje się niedostępny. Dla dystrybutora kosztowne jest ściąganie starych bibliotek do nowej wersji dystrybucji. Po pewnym czasie problem powtórzył się również w dystrybucjach SUSE.
W wyniku tego dla niektórych dystrybucji trzeba było zrezygnować z wyraźnej zależności od ncurses-libs, a produkt poprawić tak, aby mógł współpracować z dowolną wersją biblioteki.
Nota bene, w 8. wersji Red Hat nie ma już metapakietu python, który odnosił się do starego dobrego python 2.7. Jest python2 i python3.
Alternatywa dla menedżerów pakietów
Problem z zależnościami jest stary i od dawna oczywisty. Wspomnijmy chociażby o Dependency hell.
Połączyć różnorodne biblioteki i aplikacje tak, aby wszystkie stabilnie działały i nie kolidowały — to właśnie to zadanie próbuje rozwiązać każdy dystrybutor Linux.
Zupełnie inaczej próbuje rozwiązać ten problem menedżer pakietów Snappy od Canonical. Główna idea: aplikacja działa w izolowanej i chronionej od głównego systemu piaskownicy. Jeśli aplikacja potrzebuje bibliotek, są one dostarczane razem z samą aplikacją.
Flatpak umożliwia również uruchamianie aplikacji w piaskownicy, korzystając z Linux Containers. Pomysł piaskownicy wykorzystuje również AppImage.
Te rozwiązania pozwalają na tworzenie jednego pakietu dla wszystkich dystrybucji. W przypadku Flatpak instalacja i uruchomienie aplikacji są możliwe nawet bez wiedzy administratora.
Główny problem polega na tym, że nie wszystkie aplikacje mogą działać w piaskownicy. Niektóre potrzebują bezpośredniego dostępu do platformy. Nie mówiąc już o modułach jądra, które są ściśle zależne od jądra i w żaden sposób nie pasują do koncepcji piaskownicy.
Drugi problem – popularne w środowisku enterprise dystrybucje od Red Hat i SUSE jeszcze nie zawierają wsparcia dla Snappy i Flatpak.
W związku z tym Veeam Agent for Linux nie jest dostępny ani na ani na .
Na zakończenie pytania o menedżery pakietów zauważam, że istnieje możliwość całkowitego ich zaniechania, łącząc w jeden pakiet pliki binarne i skrypt do ich instalacji.
Taki bundle pozwala stworzyć jeden wspólny pakiet dla różnych dystrybucji i platform, przeprowadzając interaktywny proces instalacji z niezbędną personalizacją. Spotkałem się z takimi pakietami dla systemu Linux tylko od VMware.
Problem z aktualizacjami

Nawet jeśli wszystkie problemy z zależnościami zostały rozwiązane, program może działać różnie na tej samej dystrybucji. Chodzi o aktualizacje.
Istnieją 3 strategie aktualizacji:
- Najprostsza – nigdy nie aktualizować. Skonfigurowałem serwer i zapomniałem. Po co aktualizacje, skoro wszystko działa? Problemy zaczynają się przy pierwszym kontakcie z pomocą techniczną. Twórca dystrybucji wspiera tylko zaktualizowaną wersję.
- Można zaufać dystrybutorowi i skonfigurować automatyczne aktualizacje. W takim przypadku kontakt z pomocą techniczną jest prawdopodobny zaraz po nieudanej aktualizacji.
- Opcja ręcznej aktualizacji dopiero po przetestowaniu na infrastrukturze testowej – jest najpewniejsza, ale kosztowna i pracochłonna. Niewiele osób może sobie na nią pozwolić.
Ponieważ różni użytkownicy stosują różne strategie aktualizacji, konieczne jest wsparcie zarówno najnowszej wersji, jak i wszystkich wcześniejszych wydań. To komplikuje zarówno proces rozwoju, jak i testowania, zwiększając ból głowy zespołu wsparcia.
Różnorodność platform sprzętowych
Różne platformy sprzętowe to problem w dużej mierze specyficzny dla kodu native. Co najmniej trzeba zbierać binarki dla każdej wspieranej platformy.
W projekcie Veeam Agent for Linux nie możemy wciąż wspierać niczego opartego na RISC.
Nie będę szczegółowo zatrzymywał się na tym zagadnieniu. Wskazę jedynie główne problemy: rodzaje zależne od platformy, takie jak size_t, wyrównanie struktur i kolejność bajtów.
Linkowanie statyczne i/lub dynamiczne

A kwestia „Jak linkować z bibliotekami — dynamicznie czy statycznie?” wymaga omówienia.
Zazwyczaj aplikacje w C/C++ na Linuxie korzystają z dynamicznego linkowania. Działa to świetnie, jeśli aplikacja jest zbudowana specjalnie dla danej dystrybucji.
Jeśli celem jest objęcie różnorodnych dystrybucji jednym plikiem binarnym, należy kierować się najstarszą wspieraną dystrybucją. Dla nas jest to Red Hat 6. Zawiera on gcc 4.4, który nawet standard C++11 wspiera nie .
Budujemy nasz projekt przy użyciu gcc 6.3, który w pełni wspiera C++14. Oczywiście, w takim przypadku na Red Hat 6 bibliotekę libstdc++ i boost trzeba mieć ze sobą. Najprościej jest linkować z nimi statycznie.
Niestety, nie ze wszystkimi bibliotekami można linkować statycznie.
Po pierwsze, biblioteki systemowe, takie jak libfuse, libblkid należy linkować dynamicznie, aby mieć pewność, że są zgodne z jądrami i jego modułami.
Po drugie, jest kwestia licencji.
Licencja GPL w zasadzie zezwala na linkowanie bibliotek tylko z kodem opensource. MIT i BSD zezwalają na linkowanie statyczne i pozwalają na dołączanie bibliotek do projektu. Z kolei LGPL zdaje się, że nie sprzeciwia się linkowaniu statycznemu, ale wymaga udostępnienia plików potrzebnych do linkowania.
W ogólnym przypadku użycie linkowania dynamicznego ochroni przed koniecznością udostępniania czegokolwiek.
Kompilacja aplikacji C/C++
Aby skompilować aplikacje C/C++ dla różnych platform i dystrybucji, wystarczy dobrać lub skompilować gcc odpowiedniej wersji i skorzystać z krzyżowych kompilatorów dla specyficznych architektur, skompilować cały zestaw bibliotek. Praca ta jest całkowicie wykonalna, ale dość uciążliwa. I nie ma pewności, że wybrany kompilator i biblioteki zapewnią działającą wersję.
Oczywistą zaletą jest znaczne uproszczenie infrastruktury, ponieważ cały proces kompilacji można wykonać na jednej maszynie. Ponadto wystarczy skompilować jeden zestaw plików binarnych dla jednej architektury, a następnie można je spakować w paczki dla różnych dystrybucji. W ten sposób tworzone są paczki veeam dla Veeam Agent for Linux.
W przeciwieństwie do tej opcji, można po prostu przygotować farmę do budowania, czyli kilka maszyn do kompilacji. Każda taka maszyna zapewni kompilację aplikacji oraz stworzenie pakietu dla konkretnego dystrybucji i określonej architektury. W takim przypadku kompilacja odbywa się za pomocą narzędzi przygotowanych przez dystrybutora. To znaczy, etap przygotowania kompilatora i dobór bibliotek odpada. Ponadto, proces budowy może być łatwo zrównoleglony.
Są jednak też wady takiego podejścia: dla każdej dystrybucji w ramach tej samej architektury trzeba będzie zbierać swój zestaw plików binarnych. Kolejną wadą jest to, że takie wiele maszyn wymaga obsługi, przydzielenia dużej ilości miejsca na dysku oraz pamięci operacyjnej.
W ten sposób zbierane są pakiety KMOD modułu jądra veeamsnap pod dystrybucje Red Hat.
Open Build Service
Koledzy z SUSE próbowali wprowadzić pewnego rodzaju złoty środek w postaci specjalnej usługi do kompilacji aplikacji i budowy pakietów — .
Jest to de facto hypervisor, który tworzy maszynę wirtualną, instaluje w niej wszystkie niezbędne pakiety, przeprowadza kompilację aplikacji oraz tworzy pakiet w tym izolowanym środowisku, po czym taka maszyna wirtualna zostaje zwolniona.

Zaimplementowany w OpenBuildService harmonogram samodzielnie określi, ile maszyn wirtualnych może uruchomić dla optymalnej prędkości budowy pakietów. Wbudowany mechanizm podpisywania automatycznie podpisze pakiety i umieści je w wbudowanym repozytorium. Wbudowany system kontroli wersji zapisze historię zmian i budowy. Wystarczy jedynie dodać do tego systemu swoje źródła. Nie ma potrzeby uruchamiania własnego serwera, można skorzystać z otwartego.
Jest jednak problem: taki kombajn trudno wkomponować w istniejącą infrastrukturę. Na przykład, kontrola wersji nie jest potrzebna, mamy już swoją dla źródeł. Nasz mechanizm podpisywania różni się: wykorzystujemy specjalny serwer. Repozytorium również nie jest potrzebne.
Ponadto wsparcie dla innych dystrybucji — na przykład Red Hat — jest dość ubogie, co w pełni można zrozumieć.
Zaletą tej usługi jest szybkie wsparcie dla kolejnej wersji dystrybucji SUSE. Do oficjalnego ogłoszenia o wydaniu, niezbędne pakiety do kompilacji są publikowane w publicznym repozytorium. Na liście dostępnych dystrybucji w OpenBuildService pojawia się nowa. Zaznaczamy pole, a ona jest dodawana do planu kompilacji. W ten sposób dodanie nowej wersji dystrybucji odbywa się praktycznie za jednym kliknięciem.
W naszej infrastrukturze z użyciem OpenBuildService przygotowywane są wszystkie różnorodne pakiety KMP modułu jądra veeamsnap dla dystrybucji SUSE.
Następnie chciałbym skupić się na kwestiach specyficznych dla modułów jądra.
kernel ABI
Moduły jądra Linux historycznie były udostępniane w postaci kodu źródłowego. Twórcy jądra nie obciążają się dbaniem o wsparcie stabilnego API dla modułów jądra, a tym bardziej na poziomie binarnym, później kABI.
Aby skompilować moduł dla waniliowego jądra, niezbędne są nagłówki właśnie tego jądra, a działać będzie tylko na tym jądrze.
DKMS umożliwia automatyzację procesu kompilacji modułów przy aktualizacji jądra. W wyniku tego użytkownicy repozytoriów Debiana (i jego licznych kuzynów) korzystają z modułów jądra albo z repozytoriów dystrybutorów, albo kompilowanych ze źródeł przy użyciu DKMS.
Jednak taka sytuacja niezbyt zadowala segment Enterprise. Dystrybutorzy kodu zastrzeżonego chcą rozprowadzać produkt w postaci gotowych binariów.
Administratorzy nie chcą trzymać narzędzi deweloperskich na serwerach produkcyjnych ze względów bezpieczeństwa. Dystrybutorzy Enterprise Linux, tacy jak Red Hat i SUSE, zdecydowali, że dla swoich użytkowników będą mogły wspierać stabilne kABI. W rezultacie pojawiły się pakiety KMOD dla Red Hat oraz pakiety KMP dla SUSE.
Istota tego rozwiązania jest dość prosta. Dla konkretnej wersji dystrybucji API jądra jest zamrażane. Dystrybutor ogłasza, że wykorzystuje konkretne jądro, na przykład 3.10, i wprowadza tylko poprawki oraz usprawnienia, które w żaden sposób nie wpływają na interfejsy jądra, a moduły skompilowane dla pierwszego jądra mogą być wykorzystywane dla wszystkich następnych bez rekompilacji.
Red Hat ogłasza, że kABI jest zgodne przez cały okres życia dystrybucji. Oznacza to, że zbudowany moduł dla RHEL 6.0 (wydanie z listopada 2010 roku) powinien również działać na wersji 6.10 (wydanie z czerwca 2018 roku). To niemal 8 lat. Oczywiście, to dość skomplikowane zadanie.
Odnotowaliśmy kilka przypadków, kiedy z powodu problemów ze zgodnością kABI moduł veeamsnap przestał działać.
Po tym, jak moduł veeamsnap, zbudowany dla RHEL 7.0, okazał się niespójny z jądrem z RHEL 7.5, a jednocześnie ładował się i gwarantowanie zawieszał serwer, zrezygnowaliśmy z używania zgodności kABI dla RHEL 7 w ogóle.
Obecnie pakiet KMOD dla RHEL 7 zawiera zestaw dla każdej wersji wydania oraz skrypt, który zapewnia ładowanie modułu.
SUSE podeszło do zadania zgodności kABI bardziej ostrożnie. Zapewniają zgodność kABI tylko w ramach jednego service pack.
Na przykład wydanie SLES 12 miało miejsce we wrześniu 2014 roku. A SLES 12 SP1 już w grudniu 2015 roku, co oznacza, że minęło nieco ponad rok. Mimo że oba wydania wykorzystują jądro 3.12, są one kABI niespójne. Oczywiste jest, że utrzymanie zgodności kABI przez jedynie rok jest znacznie prostsze. Roczny cykl aktualizacji modułu jądra nie powinien sprawiać problemów twórcom modułów.
W wyniku takiej polityki SUSE nie odnotowaliśmy żadnych problemów ze zgodnością kABI w naszym module veeamsnap. Prawda jest taka, że liczba pakietów dla SUSE jest niemal dziesięciokrotnie większa.
Łatki i backporty
Mimo że dystrybutorzy starają się zapewnić zgodność kABI oraz stabilność jądra, także starają się poprawić wydajność i usunąć wady tego stabilnego jądra.
Przy tym, oprócz własnej «pracy nad błędami», programiści jądra enterprise linux śledzą zmiany w jądrze waniliowym i przenoszą je do swojego «stabilnego».
Czasami prowadzi to do nowych .
W ostatnim wydaniu Red Hat 6, w jednym z drobnych aktualizacji, wystąpił błąd. Prowadził on do tego, że moduł veeamsnap gwarantowanie zawieszał system podczas zwalniania migawki. Porównując źródła jądra przed i po aktualizacji, odkryliśmy, że winowajcą był backport. Podobny fix został wprowadzony w waniliowym jądrze wersji 4.19. Tylko że w waniliowym jądrze ten fix działał poprawnie, a przy przenoszeniu go do «stabilnego» 2.6.32 wystąpił problem ze spin-lockiem.
Oczywiście, błędy zdarzają się wszystkim i zawsze, ale czy warto było przenosić kod z 4.19 do 2.6.32, ryzykując stabilność?.. Nie jestem pewny...
Najgorsze jest to, gdy do przeciągania liny „stabilność”„modernizacja” włącza się marketing. Dział marketingu potrzebuje, aby jądro zaktualizowanego dystrybucji było stabilne z jednej strony, a z drugiej, aby było lepsze pod względem wydajności i miało nowe funkcje. To prowadzi do dziwnych kompromisów.
Kiedy spróbowałem skompilować moduł na jądrze 4.4 z SLES 12 SP3, z zdziwieniem odkryłem, że ma funkcjonalność z waniliowego 4.8. Moim zdaniem, implementacja blokowego wejścia/wyjścia jądra 4.4 z SLES 12 SP3 bardziej przypomina jądro 4.8 niż poprzednią wersję stabilnego 4.4 jądra z SLES 12 SP2. Nie podejmuję się oceny, jaki procent kodu został przeniesiony z jądra 4.8 do SLES-owego 4.4 dla SP3, jednak nie potrafię uznać jądra za wciąż stabilne 4.4.
Najbardziej nieprzyjemne jest to, że przy pisaniu modułu, który miałby działać równie dobrze na różnych jądrach, nie można już opierać się na wersji jądra. Trzeba też brać pod uwagę dystrybucję. Dobrze, że czasem można się oprzeć na definicji, która pojawia się wraz z nową funkcjonalnością, jednak taka możliwość nie zawsze jest dostępna.
W rezultacie kod obrosnął dziwacznymi dyrektywami kompilacji warunkowej.
Pojawiają się też łatki, które zmieniają udokumentowane API jądra.
Natknąłem się na dystrybucję 5.16 i byłem bardzo zaskoczony, widząc, że wywołanie lookup_bdev w tej wersji jądra zmieniło listę parametrów wejściowych.
Aby skompilować, musiałem dodać skrypt do makefile, który sprawdza, czy parametr mask jest obecny w funkcji lookup_bdev.
Podpisy modułów jądra
Ale wróćmy do pytania o dystrybucję pakietów.
Jedną z zalet stabilnego kABI jest to, że moduły jądra w postaci pliku binarnego można podpisać. W takim przypadku deweloper może być pewny, że moduł nie został przypadkowo uszkodzony ani celowo zmieniony. Można to sprawdzić poleceniem modinfo.
Dystrybucje Red Hat i SUSE umożliwiają sprawdzenie podpisu modułu i załadowanie go tylko wtedy, gdy w systemie zarejestrowany jest odpowiedni certyfikat. Certyfikat jest publicznym kluczem, którym podpisywany jest moduł. Rozprowadzamy go w postaci oddzielnego pakietu.
Problem polega na tym, że certyfikaty mogą być albo wbudowane w jądro (ich używają dystrybutorzy), albo muszą być zapisane w nieulotnej pamięci EFI za pomocą narzędzia mokutil. Narzędzie mokutil podczas instalacji certyfikatu wymaga ponownego uruchomienia systemu i jeszcze przed załadowaniem jądra systemu operacyjnego oferuje administratorowi zgodę na załadowanie nowego certyfikatu.
Tak więc dodanie certyfikatu wymaga fizycznego dostępu administratora do systemu. Jeśli maszyna znajduje się w chmurze lub po prostu w zdalnym serwerze i dostęp jest tylko przez sieć (na przykład przez ssh), dodanie certyfikatu będzie niemożliwe.
EFI na maszynach wirtualnych
Mimo że EFI jest już długo wspierane przez prawie wszystkich producentów płyt głównych, administrator może nie pomyśleć o potrzebie EFI podczas instalacji systemu, i może być ono wyłączone.
Nie wszyscy hypervisorzy wspierają EFI. VMWare vSphere wspiera EFI od wersji 5.
Microsoft Hyper-V również zyskał wsparcie dla EFI, od Hyper-V dla Windows Server 2012R2.
Jednak w domyślnej konfiguracji ta funkcjonalność dla maszyn Linux jest wyłączona, co oznacza, że certyfikatu nie można zainstalować.
W vSphere 6.5 opcję Secure Boot można ustawić tylko w starej wersji interfejsu sieciowego, który działa przez Flash. Interfejs lokalny w HTML-5 jest wciąż znacznie opóźniony.
Eksperymentalne dystrybucje
Na zakończenie rozważmy kwestię eksperymentalnych dystrybucji oraz dystrybucji bez oficjalnego wsparcia. Z jednej strony, takich dystrybucji raczej nie spotkasz na serwerach poważnych organizacji. Takie dystrybucje nie mają oficjalnego wsparcia. Dlatego też zapewnianie wsparcia technicznego produktu na takiej dystrybucji nie jest możliwe.
Jednakże takie dystrybucje stają się wygodną platformą do wypróbowania nowych rozwiązań eksperymentalnych. Na przykład Fedora, OpenSUSE Tumbleweed czy niestabilne wersje Debiana. Są one dość stabilne. Zawierają zawsze nowe wersje oprogramowania i zawsze nowe jądro. Za rok ta eksperymentalna funkcjonalność może znaleźć się w zaktualizowanym RHEL, SLES lub Ubuntu.
Tak więc, jeśli coś nie działa na eksperymentalnej dystrybucji — to jest to powód, aby zbadać problem i go rozwiązać. Należy być gotowym na to, że ta funkcjonalność wkrótce pojawi się na serwerach produkcyjnych użytkowników.
Obecna lista oficjalnie wspieranych dystrybucji dla wersji 3.0 jest dostępna do przestudiowania. Jednak rzeczywista lista dystrybucji, na których nasz produkt może działać, jest znacznie szersza.
Osobiście interesował mnie eksperyment z systemem operacyjnym 'Elbrus'. Po dostosowaniu pakietu veeam nasz produkt zainstalował się i działał. O tym eksperymencie pisałem na Habra w .
Wsparcie dla nowych dystrybucji trwa. Oczekujemy na premierę wersji 4.0. Wkrótce powinna pojawić się wersja beta, więc śledźcie !
Źródło: habr.com
