We wcześniejszych materiałach poznawaliśmy zupełne podstawy języka Python w systemie Linux i chyba przyznasz mi rację, że nie napisaliśmy nic konkretnego. Chociaż samo poznanie dwóch bibliotek dla administratora systemu Linux jest dość ważne i uważam, że pożyteczne. To dzięki nim jesteś w stanie wywołać dowolne polecenie w konsoli, jak i również możesz wiele instalacji zautomatyzować, dzięki przekazaniu argumentu przy uruchomieniu skryptu. Otwiera to ogromne możliwości, ale jeszcze warto parę elementów języka poznać, by móc pisać bardziej rozbudowane skrypty.

W tym materiale chcę napisać skrypt, który zautomatyzuje instalację dockera w kilku dystrybucjach. Dzięki temu przy pomocy wydania jednego polecenia wszystko automatycznie się zainstaluje i skonfiguruje tak, że pozostanie tylko z niego korzystać. Mam nadzieję, że na ten moment takie podstawy języka Python w systemie Linux są dla Ciebie wystarczające.

Niemniej jednak na wstępnie chciałbym przekazać kilka istotnych informacji.

Pierwszą z nich jest to, że kod niezupełnie jest zapisany zgodnie z zasadami minimalistycznymi. Piszę o tym, ponieważ jeżeli programujesz w języku Python wiele poznanych elementów można zrobić zupełnie inaczej, krócej. Jednak ta skrótowość na początku nauki może i wyrabia dobre nawyki, ale mocno utrudnia zrozumienie tematu. W związku z tym, że nie jest to książka, a krótki poradnik w którym chcę abyś nauczył się automatyzować podstawowe zadania. Dlatego przyjąłem zasadę, że najpierw pokaże jak robić by działało, a dopiero potem jak samemu poprawić kod. Zawsze wychodzę z założenia, najważniejsze że działa, a dopiero potem zastanawiam się co zrobić, by działało lepiej.

Drugą sprawą jest ilość dystrybucji na jakich jestem w stanie przetestować skrypt.

Niestety nie posiadam, aż tak rozległych możliwości aby zainstalować lub uruchomić przy użyciu wirtualizacji wszystkich dystrybucji na które dostępny jest docker. Dość dużo wykorzystuję wirtualne maszyny i niektórych nie mogę się od tak pozbyć by zwolnić miejsce. W związku z tym testy przeprowadzę na dystrybucjach Debian oraz Red Hat Enterprise Linux. Jednak obiecuję, że jeżeli kiedyś przerodzi się to w coś bardziej złożonego niż kilka artykułów postaram się o większą ilość zarówno przykładów, jak i testów systemowych. Może coś zautomatyzujemy na Windows 😉 ?

Trzecia sprawa… Dla wygody w poprzednim kodzie używałem języka polskiego. W programowaniu tego się nie robi. Używamy języka angielskiego zarówno do nazywania obiektów jak i opisów. W związku z tym w tym skrypcie używać będziemy języka angielskiego.

Widzę, że wyszedł dość długi wstęp dlatego, aby dalej nie zanudzać, przejdźmy do kodu i podstawy języka Python w systemie Linux.

 

Zapis na newsletter!

Zanim jednak rozwinę temat dalej to jedna informacja na początek. Prowadzę regularny, cotygodniowy newsletter z poradami związanymi z Zabbixem, monitoringiem oraz dobrymi praktykami w IT. Jeżeli nie chcesz przegapić kolejnych części oraz zaproszeń na nasze darmowe szkolenia to zapraszam do zapisu. Wystarczy pobrać mój poradnik. Monitoring IT – Dobre Praktyki. Wystarczy wejść klikając na obrazek:

__________________________________________

 

Podstawy języka Python w systemie Linux – SŁOWNIKI

W języku Python istnieje takie coś jak słownik. Polega na przypisaniu jakieś wartości w postaci ciągu znaków lub w postaci liczbowej. My w naszym skrypcie stworzymy taki słownik, w którym przypiszemy do systemów odpowiedniego menadżera pakietów.

Tworzony przez nas skrypt będzie instalował wersję obsługiwaną przez wiersz poleceń. Dokładny opis znajduje się pod adresem https://docs.docker.com/engine/install/.

Zgodnie z rozpiską dostępną na stronie docker oficjalnie obsługuje dystrybucje to centos, fedora, rhel, sles, debian i ubuntu. Naszym celem jest do każdego z systemów przypisanie odpowiedniego menadżera pakietów. Robimy to w następujący sposób:

systemList = {'centos' : 'yum', 'fedora' : 'dnf', 'rhel' : 'yum', 'sles' : 'zypper', 'debian' : 'apt', 'ubuntu' : 'apt'}

Bardzo podobne do list, które tworzyliśmy nie tak dawno temu. Różnica jest taka, że w cudzysłowu określamy nazwę etykiety pod jaką dostępna będzie informacja znajdująca się po dwukropku. Takich elementów w jednym słowniku możemy stworzyć kilkanaście, co widać w powyższym przykładzie. Aby wydobyć wartość ze słownika musimy użyć sformułowania:

systemList['debian']

Czyli używasz nazwy jaką nadałeś słownikowi. Następnie w nawiasie kwadratowym wprowadzasz etykietę z jakiej pozyskać dane. Pamiętaj, że musisz nazwę objąć w cudzysłów pojedynczy lub podwójny w tym wypadku nie ma to znaczenia. Jeżeli o tym zapomnisz to Python zinterpretuje to jako zmienną z której będzie chciał pobrać nazwę etykiety.

Stwórzmy prosty skrypt. Nazwałem go slowniki.py i zawiera kod:

#!/usr/bin/env python3
systemList = {'centos' : 'yum', 'fedora' : 'dnf', 'rhel' : 'yum', 'sles' : 'zypper', 'debian' : 'apt', 'ubuntu' : 'apt'}
print(systemList['debian']) 

Słownik używam w instrukcji print ponieważ jeżeli wpiszesz ją jak w poprzednim przykładzie, nic się z nią nie będzie działo. Jeżeli chcesz możesz jeszcze stworzyć zmienną i do niej przypisać wartość słownika. Następnie go wyświetlić.

Pozostało uruchomić stworzony skrypt:

python3 slowniki.py
apt

Jako wynik otrzymaliśmy apt. Myślę, że rozumiesz. Słownik służy do grupowania elementów związanych ze sobą w jeden element. Brzmi podobnie jak lista, lecz w tym wypadku posługujemy się nazwami, a nie cyframi.

Przygotowanie elementów

Jeżeli zerknąłeś na listę dostępnych dystrybucji to możliwe, że zauważyłeś. Można je podzielić na dwie grupy. Pierwsza z nich jest to Red Hat lub opartych o niego dystrybucji. Natomiast druga to system Debian i Ubuntu.

Pierwszą czynnością jaką musimy wykonać względem poradnika to odinstalować jego stare wersje. Jednak możemy to, co odinstalowujemy pogrupować i rozdzielić pomiędzy dwie listy.

Pierwsza dotycząca Red Hat Enterprise Linux i oparte o niego:

dockerRemoveRH = ['docker', 'docker-client', 'docker-client-latest', 'docker-common', 'docker-latest', 'docker-latest-logrotate', 'docker-logrotate', 'docker-engine', 'docker-selinux', 'docker-engine-selinux', 'podman', 'runc']

Druga lista związana z systemem Debian:

dockerRemoveDEB = ['docker', 'docker-engine', 'docker.io', 'containerd', 'runc']

Listy omawialiśmy poprzednio, dlatego przykłady pozostawiam bez opisu. Myślę, że wszystko jest jasne.

Skąd o tym wszystkim wiem? Otóż większość z tych systemów znam, dlatego też byłem w stanie wzrokowo je pogrupować. Natomiast zawsze warto jest się rozejrzeć po dokumentacji. Wspomoże to utworzenie automatyzacji, tak jak w naszym przypadku. Natomiast zarówno w jednej, jak i w drugiej liście znajduje się oprogramowanie które należy odinstalować przed instalacją nowego. Jednak jeżeli przeanalizujesz sam, przechodząc po poszczególnych dystrybucjach Red Hata to zauważysz, że nie zawsze w każdym z nich usuwa się te same pakiety. Pomimo tego stworzyłem tablice, w której umieszczone są wszystkie pakiety do odinstalowania. Jednak dodatkowo dodałem następujący opis:

# from 0 to 7 - CentOS
# from 0 to 9 - Fedora
# from 0 to end - RHEL
# from 0 to 7 and 11 – SLES

Jest on bardzo istotny ponieważ określa które pakiety w jakim systemie należy usunąć. Wszystkie te informacje pozyskałem z dokumentacji dockera i zapisałem dla własnej wygody jak i pozostałych użytkowników skryptu.

Cały obecny kod powinien wyglądać następująco:

#!/usr/bin/env python3
import subprocess
import argparse


systemList = {'centos' : 'yum', 'fedora' : 'dnf', 'rhel' : 'yum', 'sles' : 'zypper', 'debian' : 'apt', 'ubuntu' : 'apt'}

# from 0 to 7 - CentOS
# from 0 to 9 - Fedora
# from 0 to end - RHEL
# from 0 to 7 and 11 - SLES

dockerRemoveRH = ['docker', 'docker-client', 'docker-client-latest', 'docker-common', 'docker-latest', 'docker-latest-logrotate', \
'docker-logrotate', 'docker-engine', 'docker-selinux', 'docker-engine-selinux', 'podman', 'runc']

# Ubuntu and Debian

dockerRemoveDEB = ['docker', 'docker-engine', 'docker.io', 'containerd', 'runc']

Dodałem od razu dwie poznane w poprzednim materiale biblioteki, ponieważ w późniejszym czasie będziemy z nich korzystali. Tak jak wspomniałem, dodałem komentarze, które wspomogą mnie oraz innych w celu zrozumienia, co autor miał na myśli.

Przekazanie argumentu przy uruchomieniu skryptu

Tym zajmowaliśmy w jednej z poprzedniej części. Ten moduł wykorzystamy również w przypadku tego skryptu. Zerknijmy na funkcję którą powinieneś już znać:

def callOptions():
	system = argparse.ArgumentParser()
	system.add_argument("-s", "--system", dest="chosenSystem", help="Write a name of system witch you are using. \
		You can choose from: CentOS, Debian, Fedora, RHEL, SLES and Ubuntu")
	options = system.parse_args()
	if not options.chosenSystem:
		system.error('You haven\'t specified the system you are using!')
	return options

Do zmian należy zaliczyć, po pierwsze język angielski w całej funkcji. Mam nadzieję, że nie popełniłem jakieś błędu językowego :). Chcę zaznaczyć, że w etykiecie help ważne jest, aby zawsze zawrzeć istotne informacje dla opcji. Jeżeli ktoś będzie korzystał z twojego skryptu to nie jest jasnowidzem i nie będzie wiedział jaką wartość ma lub może wprowadzić. Dlatego bardzo istotna jest ta informacja.

Następnym elementem jest to znak \ który znajduje się w opisie. Ciężko jest mi to w tym miejscu wytłumaczyć, ale jeżeli kod który wprowadzasz w swoim edytorze wykracza poza linie to jeżeli użyjesz tego znaku możesz pisać w następnej.

Zwróć jeszcze uwagę na kod:

system.error('You haven\'t specified the system you are using!')

Tu również skorzystaliśmy z \. Ponieważ w języku angielskim często korzysta się z ’ który służy również jako rozpoczęcie i zamknięcie ciągu znaków musimy użyć \ aby ten ’ był dołączony do treści jako tekst, a nie jako zakończenie ciągu.

Pozostałe elementy są identyczne jak te których używaliśmy do tej pory.

Teraz warto sprawdzić czy wszystko działa i dane są zapisane. Dlatego na samym końcu skryptu dodajemy:

options = callOptions()
print(options.chosenSystem)

Pozostaje uruchomić nasz skrypt w następujący sposób:

python3 docker.py -s debian
debian

Wszystko działa zgodnie z naszymi przewidywaniami. Zerknij jednak co się stanie jeżeli wprowadzisz cokolwiek po opcji -s:

python3 caly.py -s windows
windows

Zwracana będzie każda wartość jaką wprowadzimy przy uruchomieniu skryptu. My jednak chcemy by było to weryfikowane względem systemów jakie obsługuje nasz skrypt.

Weryfikacja otrzymanych danych

Możemy zweryfikować to na kilka sposobów. Natomiast ten preferowany przeze mnie polega na stworzeniu funkcji która zwraca wartość True w przypadku gdy wartość znajduje się na liście. Natomiast False gdy nie. Taka funkcja wyglądać powinna w następujący sposób:

def systemService(systemName):
	if systemName in systemList:
		return True
	else:
		return False

Opisać przykład można w następujący sposób:

Jeżeli nazwa systemu która została przekazana podczas wywołania skryptu (systemName) znajduje się w słowniku (systemList) to poinformuj, że tak jest (True). Jeżeli jednak nie znajduje się w słowniku potrzebuję również takiej informacji (False).

WAŻNE! W języku Python wartości True i False zawsze zapisuje się z dużej litery. W wielu językach te wartości zapisuje się z małej lub w dowolny sposób.

Czyli skonstruowaliśmy funkcję którą sprawdza czy przekazana wartość znajduje się na liście. Teraz musimy ją wywołać. Na samym końcu skryptu usuwamy wartość print z poprzedniego przykładu i dodajemy instrukcję warunkową:

if systemService(options.chosenSystem):
	print(f"The {options.chosenSystem} is on the list!")
else:
	print("Script does not support system you choose. See help -h for more.")

W instrukcji warunkowej sprawdzamy jaką wartość otrzymamy. Jeżeli systemService zwróci wartość True zostanie wykonany pierwszy print. Natomiast jeżeli wartość nie znajduje się na liście zostanie zwrócona wartość False i wywołana zostanie instrukcja po else. Spójrz na przykład wywołania:

python3 docker.py -s centos
The centos is on the list!

Natomiast jeżeli wprowadzisz nazwę która nie występuje na liście, to otrzymasz informację występująca po instrukcji else.

Jednak w tym wypadku spotkać się możemy z pewnym problemem:

python3 caly.py -s CEnTOS
Script does not support system you choose. See help -h for more.

Wprowadzona w ten sposób nazwa systemu powoduje, że jest ona nierozpoznawalna przez nasz skrypt. Natomiast my chcielibyśmy by tak było. I tu rozwiązań jest kilka.

Pierwsze z nich polega na skorzystaniu z funkcji zmieniającej zawartość ciągu na małe litery. Tak jak to prezentuje w poniższym przykładzie:

if systemService(options.chosenSystem.lower()):
	print(f"The {options.chosenSystem.lower()} is on the list!")
else:
	print("Script does not support system you choose. See help -h for more.")

Funkcje pogrubiłem w przykładzie. Ma ona swój minus ponieważ musimy pamiętać, aby w kilku miejscach skorzystać z tej funkcji.

Natomiast drugi sposób polega na przypisaniu wartości do zmiennej, w której zapisujemy opcje:

options = callOptions().chosenSystem.lower()

if systemService(options):
	print(f"The {options} is on the list!")
else:
	print("Script does not support system you choose. See help -h for more.")

Jednak ten sposób ma swój minus taki, że przypisaliśmy wartość tylko jednej opcji. Jak będzie ich kilka trzeba tworzyć nową zmienną.

W naszym wypadku wygodniejsze jest rozwiązanie numer dwa dlatego z niego skorzystam.

Elementy do usunięcia

Pierwszą czynnością zgodnie z zaleceniami dokumentacji to usunięcie starszych wersji dockera lub podmana. Na samym początku stworzyliśmy listy na których znajdują się nazwy pakietów do usunięcia. Teraz nadeszła pora wykorzystać powyższą listę. Dlatego napiszemy funkcję:

def redHatRemove(systemName):
	i = 1
	removeApp = ""

	if systemName == 'centos':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['centos'] + " " + "remove " + removeApp + " -y"
		return remove

Przekazywać do funkcji będziemy nazwę systemu którą podaliśmy przy uruchomieniu skryptu. Dwie dalsze linie ze zmiennymi na razie pomińmy. W bloku instrukcji if określamy, że jeżeli w przekazanej wartość do funkcji znajduje się centos to zostanie wykonana pętla while.

O pętlach jeszcze nie rozmawialiśmy. Są one dostępne w każdym języku programowania. Polegają na wykonywaniu wskazanych czynności aż warunek nie zostanie spełniony. Na samym początku funkcji znajduje się zmienna z wartością 1. Natomiast gdy korzystamy z pętli mamy instrukcję w której wartość i nie równa się 8. Nagłówek pętli while możemy opisać jako:

Jeżeli wartość znajdująca się w i nie równa się 8 wykonaj obieg pętli.

Następnie funkcje w pętli są wykonywane. Na samym końcu pętli znajduje się i do którego dodajemy +1. Czyli wracamy ponownie do początku pętli while. Tym razem wartość zmiennej i wynosi 2. Nadal nie równa się 8 dlatego też wykonywany jest kolejny obieg pętli. I tak aż nie osiągnie wartości 8.

Dlaczego użyłem tu wartości 8. Otóż jeżeli zerkniesz na opis o którym wspominałem:

# from 0 to 7 - CentOS 

Pakiety dotyczące centos znajdują się na liście od 0 do 7. W związku z tym, że liczymy od 0 wartości jest 8, a nie 7. I teraz co ważniejsze gdy wartość i osiągnie 8 to zostanie tylko sprawdzony warunek znajdujący się w pętli, a następnie przekazana zostanie wartość True i pętla zakończy swoje działanie.

Po co korzystamy z pętli?

Możliwe, że się domyślasz. Do zmiennej removeApp przypisujemy tylko te pakiety, które mają zostać usunięte przy danej dystrybucji.

Następnie w zmiennej remove konstruujemy polecenie służące usunięciu pakietów.

Jeżeli chcesz zobaczyć jak to wygląda, instrukcję na samym dole skryptu zmodyfikuj w następujący sposób:

if systemService(options):
	print(redHatRemove(options))
else:
	print("Script does not support system you choose. See help -h for more.")

I następnie uruchom:

python3 caly.py -s centos
sudo yum remove docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine  -y

Jest to kompletne polecenie, dzięki któremu jesteśmy w stanie usunąć stare paczki dockera przed instalacją nowych.

W związku z powyższym cała nasza funkcja do usuwania zbędnych pakietów, dla wszystkich systemów Red Hat powinna wyglądać w następujący sposób:

def redHatRemove(systemName):
	i = 1
	removeApp = ""

	if systemName == 'centos':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['centos'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'fedora':
		while i != 10:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['fedora'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'rhel':
		while i != len(dockerRemoveRH):
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['rhel'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'sles':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['sles'] + " " + "remove " + removeApp + dockerRemoveRH[11] + " -y"
		return remove

Dla Ciebie pozostawiam przeanalizowanie pozostałych opcji. Są one prawie w identyczny sposób skonstruowane. Jedynie w przypadku sles występuje pewna różnica wynikająca z tego, że jeden z pakietów znajduje się na końcu listy.

W przypadku Debiana mamy mniej i usuwamy te same pakiety, dlatego funkcja jest znacznie prostsza:

def debianRemove():
	i = 1
	removeApp = ""

	while i != len(dockerRemoveDEB):
		removeApp += dockerRemoveDEB[i] + " "
		i += 1

	remove = 'sudo ' + systemList['debian'] + " " + "remove " + removeApp + " -y"
	return remove

Funkcja wygląda prawie identycznie jak poprzednia z tą różnicą, że nie przekazujemy do niej żadnej wartości. Nie ma sensu tego robić ponieważ czy będzie to Debian czy Ubuntu usuwane pakiety, jak i polecenie jest identyczne. To, co może zaciekawić to to, że w pętli while użyłem funkcji len, dzięki której otrzymamy dokładną liczbę elementów znajdujących się na liście.

Stworzyliśmy dwie funkcje które po wywołaniu usuną stare oprogramowanie dockera i podmana. Jednak w całym naszym skrypcie nie ma miejsca w którym funkcje są wywoływane. Co ważniejsze są to dwie funkcje które wywołanie uzależnione jest od wybranej dystrybucji. W związku z tym stwórzmy oddzielną funkcje w której stworzymy instrukcję warunkową. W tej instrukcji warunkowej wywołamy jedną z dwóch funkcji w zależności od podanej dystrybucji. Wspomniana funkcja wygląda następująco:

def remove(systemName):
	if (systemName == 'debian') or (systemName == 'ubuntu'):
		subprocess.call(debianRemove(), shell=True)
	if (systemName == 'centos') or (systemName == 'fedora') or (systemName == 'rhel'):
		subprocess.call(redHatRemove(systemName), shell=True)

W tym przypadku specjalnie stworzyłem dwie oddzielne instrukcje if. Musisz zapamiętać, że jeżeli korzystasz z operatora typu or przy wykonywaniu porównania zawsze muszą być wartości po dwóch stronach znaku porównywania. Chodzi mi tutaj o to, że powyższy przykład zgodnie z pewną logiką można zapisać mniej więcej tak:

	if systemName == 'debian' or 'ubuntu':
		subprocess.call(debianRemove(), shell=True)

Wygląda ona bardzo podobnie, ale jest błędna. Pamiętacie, że wynikiem porównania jest zawsze True albo False. W związku z tym po or wartość zawsze będzie True ponieważ nie porównujemy nią z niczym. W związku z tym jeżeli instrukcja warunkowa będzie skonstruowana w taki sposób, niezależnie od przesłanego wyniku instrukcje w niej zawarte zawsze się wykonają.

Jeżeli chodzi o nawiasy użyte we właściwym przykładzie, są one opcjonalne. Jednak poprawiają  czytelność kodu dlatego polecam je stosować przy tak złożonych porównaniach.

Teraz należy naszą funkcję dodać do końcowej instrukcji warunkowej:

if systemService(options):
	remove(options)
else:
	print("Script does not support system you choose. See help -h for more.") 

Kod zajmujący się usuwaniem zbędnych pakietów mamy już stworzony.

W razie jakichkolwiek problemów cały dotychczasowy kod pokazuję poniżej:

#!/usr/bin/env python3
import subprocess
import argparse


systemList = {'centos' : 'yum', 'fedora' : 'dnf', 'rhel' : 'yum', 'sles' : 'zypper', 'debian' : 'apt', 'ubuntu' : 'apt'}

# from 0 to 7 - CentOS
# from 0 to 9 - Fedora
# from 0 to end - RHEL
# from 0 to 7 and 11 - SLES

dockerRemoveRH = ['docker', 'docker-client', 'docker-client-latest', 'docker-common', 'docker-latest', 'docker-latest-logrotate', \
'docker-logrotate', 'docker-engine', 'docker-selinux', 'docker-engine-selinux', 'podman', 'runc']

# Ubuntu and Debian

dockerRemoveDEB = ['docker', 'docker-engine', 'docker.io', 'containerd', 'runc']

def callOptions():
	system = argparse.ArgumentParser()
	system.add_argument("-s", "--system", dest="chosenSystem", help="Write a name of system witch you are using. \
		You can choose from: CentOS, Debian, Fedora, RHEL, SLES and Ubuntu")
	opcje = system.parse_args()
	if not opcje.chosenSystem:
		system.error('You haven\'t specified the system you are using!')
	return opcje

def systemService(systemName):
	if systemName in systemList:
		return True
	else:
		return False

def redHatRemove(systemName):
	i = 1
	removeApp = ""

	if systemName == 'centos':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['centos'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'fedora':
		while i != 10:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['fedora'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'rhel':
		while i != len(dockerRemoveRH):
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['rhel'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'sles':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['sles'] + " " + "remove " + removeApp + dockerRemoveRH[11] + " -y"
		return remove

def remove(systemName):
	if (systemName == 'debian') or (systemName == 'ubuntu'):
		subprocess.call(debianRemove(), shell=True)
	if (systemName == 'centos') or (systemName == 'fedora') or (systemName == 'rhel'):
		subprocess.call(redHatRemove(systemName), shell=True)

options = callOptions().chosenSystem.lower()

if systemService(options):
	remove(options)
else:
	print("Script does not support system you choose. See help -h for more.") 

Instalowanie wstępnych pakietów

Następnym zaleceniem w dokumentacji dockera jest instalacja niezbędnych pakietów przed jego instalacją. Do tego celu stwórzmy funkcje, która niezależnie od menadżera pakietów przeprowadzi taką:

def install(systemName, instalactionPackages):
	subprocess.call("sudo " + systemList[systemName] + " install " + instalactionPackages + " -y", shell=True)

W funkcji przekazujemy dwie wartości. Pierwsza z nich to nazwa systemu, druga to nazwa pakietów jakie chcemy zainstalować. Część odpowiedzialna za wykonanie nie powinna zaskoczyć. Jest ona bardzo podobna do tych poznanych wcześniej.

Pozostało stworzyć funkcje, w której zostanie wykonana instalacja dla poszczególnych dystrybucji. Robimy to w następujący sposób:

def dockerInstall(systemName):
	if systemName == 'centos':
		install(systemName, 'yum-utils')
	elif systemName == 'fedora':
		install(systemName, 'dnf-plugins-core')	
	elif systemName == 'rhel':
		install(systemName, 'yum-utils')
	elif systemName == 'sles':
		pass

Prezentowana część dotyczy systemu Red Hat oraz jemu pokrewnych, ponieważ część odpowiedzialna za Debiana i Ubuntu musi posiadać „coś jeszcze”. Jak sam widzisz sama funkcja i instrukcja w niej zawarta nie jest niczym nowym. Do funkcji instalacyjnej przekazujemy nazwę systemu, dzięki któremu uzyskamy nazwę menadżera pakietów oraz nazwę pakietu jaki chcemy zainstalować. Jakie pakiety powinny być zainstalowane znajdują się w dokumentacji dockera. Tak jak wspomniałem na wstępie tego artykułu. Nie każdą dystrybucję znam i tak jak w przypadku sles czyli OpenSuse w instrukcji wpisałem pass. Słowo pass po wybraniu dystrybucji sles pozwoli przejść do dalszych instrukcji powiadamiając Pythona, że co miał zrobić to już zrobił. Nie chcę podejmować się instalacji w dystrybucji z której nigdy nie korzystałem.

A teraz co z Debianem i Ubuntu?

W dystrybucjach opisywanych przed chwilą pobranie aktualnej listy pakietów jak i aktualizacja jest to jedno polecenia. Natomiast w Debianie oraz dystrybucjach opartych o niego są to dwa oddzielne polecenia. W związku z tym stwórzmy oddzielną funkcję:

def debUpdate():
	subprocess.call("sudo apt update", shell=True)

Prosta funkcja która myślę, że nie wymaga większych wyjaśnień.

Teraz dodajmy Debiana i Ubuntu do naszej listy:

      elif systemName == 'debian’:

            debUpdate()

            install(systemName, 'ca-certificates curl gnupg lsb-release’)

      elif systemName == 'ubuntu’:

            debUpdate()

            install(systemName, 'ca-certificates curl gnupg lsb-release’)

Tę część instrukcji dodajemy oczywiście do instrukcji zawartej w funkcji dockerInstall. Na początku wykonujemy aktualizację repozytorium, a następnie instalujemy wymagane oprogramowanie.

Dodawanie repozytorium

Sposobów instalacji dockera jest kilka. W tym przykładzie wybrałem wersję instalowaną z oficjalnego repozytorium dockera, w związku z tym takie repozytorium musimy na początku dodać. W przypadku dystrybucji Red Hat dodanie repozytorium jest to jedno polecenie. Natomiast w Debianie i Ubuntu jest to trochę bardziej złożona sprawa.

Na początku zajmiemy się Red Hatem i dystrybucjami z nim związanymi. Tworzymy zmienną, w której umieszczamy ciąg znaków:

redHatRepos = "sudo yum-config-manager --add-repo "

Ten ciąg znaków jest to nic innego jak polecenie, dzięki któremu dodamy odpowiednie repozytorium do systemu. Od razu dopowiem, że w przypadku Red Hat Enterprise Linux nie istnieje docker w wersji 64 bitowej w związku z tym skorzystamy z repozytorium CentOS.  Zarówno w jednej, jak i w drugiej dystrybucji występuje menadżer pakietów yum dlatego nie będzie to stanowiło jakiegoś większego wyzwania. Część instrukcji odpowiedzialna za Red Hat i dystrybucje mu pokrewne powinna wyglądać w następujący sposób:

if (systemName == 'centos') or (systemName == 'rhel'):
		install(systemName, 'yum-utils')
		subprocess.call(redHatRepos + " https://download.docker.com/linux/centos/docker-ce.repo -y", shell=True)
	elif systemName == 'fedora':
		install(systemName, 'dnf-plugins-core')	
		subprocess.call("dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo -y", shell=True)
	# elif systemName == 'rhel':
	# 	install(systemName, 'yum-utils')
	# 	subprocess.call(redHatRepos + " https://download.docker.com/linux/rhel/docker-ce.repo -y", shell=True)
	elif systemName == 'sles':
		pass

W powyższym przykładzie zastosowaliśmy porównanie z użyciem operatora or. Omawiałem to we wcześniejszej części tego artykułu dlatego nie będę się powtarzał. Instalujemy to samo repozytorium zarówno dla systemu centos jak i rhel. Jedyne co mocno kuje w oczy to instrukcja w Fedorze. Wprowadziłem ją wprost, ponieważ w żadnym innym systemie nie korzystaliśmy z menadżera pakietów dnf dlatego postanowiłem wprowadzić polecenie bezpośrednio. To samo można zrobić w przypadku yum. W instrukcji jest objęta w komentarz dlatego wystąpienie polecenia odpowiedzialnego za dodanie do repozytorium będzie wykorzystane tylko raz. Tłumacząc się, zrobiłem to specjalnie by pokazać, że tak i tak można. Osobiście preferuję opcję ze zmienną ponieważ przy modyfikacji kodu, dodając nową dystrybucję może okazać się, że będzie korzystała z konkretnego menadżera systemu.

Spójrzmy teraz jak dodać repozytoria do systemu Debian oraz Ubuntu:

def debRepoAdd(systemName):
	subprocess.call("sudo mkdir -p /etc/apt/keyrings", shell=True)
	subprocess.call('curl -fsSL https://download.docker.com/linux/' + systemName + '/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg', shell=True)
	subprocess.call('echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/"' \
		+ systemName +'" $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null', shell=True) 

Jak widzisz są to standardowe polecenia wywołane przy pomocy biblioteki subprocess. Jednak chciałbym byś szczególnie zerknął na ostatnie wywołanie subprocess. Jeżeli obejmujesz wyrażenie, ciąg znaków czy cokolwiek innego w cudzysłów pojedynczy, to wewnątrz tego cudzysłowu możesz skorzystać z podwójnego. Pamiętaj, że jeżeli użyjesz pojedynczego, to tak jakbyś zakończył poprzedni. Dlatego w tym wypadku bardzo istotne jest byś zapamiętał to.

Pamiętaj, że napisana funkcja w poprzednim przykładzie musi się znajdować nad funkcją, w której jest wywoływana. Czyli w naszym przypadku nad dockerInstall.

W tym wypadku instrukcja we wspomnianej wyżej funkcji dotycząca Debiana i Ubuntu powinna wyglądać w następujący sposób:

	elif systemName == 'debian':
		debUpdate()
		install(systemName, 'ca-certificates curl gnupg lsb-release')
		debRepoAdd('debian')
		debUpdate()
	elif systemName == 'ubuntu':
		debUpdate()
		install(systemName, 'ca-certificates curl gnupg lsb-release')
		debRepoAdd('ubuntu')
		debUpdate()

Nie będę się za dużo na tym rozpisywał, ponieważ jest to dość oczywiste. Dodam tylko, że po wywołaniu funkcji odpowiedzialnej za dodanie repozytorium musimy zaktualizować repozytoria. Robimy to przy pomocy stworzonej wcześniej funkcji.

Instalacja dockera

Nadszedł czas przejść do głównej części naszego skryptu. Tak naprawdę mamy już wszystko przygotowane. Jedyne co bym dodał, to zmienna z pakietami które musimy zainstalować:

dockerInstalaction = " docker-ce docker-ce-cli containerd.io docker-compose-plugin"

Zmienną dodaję na samej górze skryptu tam, gdzie umieściłem pozostałe. Niezależnie od systemu instalowane pakiety są takie same. Natomiast umieściłem je w zmiennej ponieważ jeżeli kiedyś zdarzyła by się jakaś zmiana to wystarczy wykonać modyfikację tylko w jednym miejscu, a nie w kilku.

Teraz do naszej instrukcji, do każdej z dystrybucji dodajemy wywołanie funkcji:

install(systemName, dockerInstalaction)

I do instrukcji końcowej dodajemy naszą funkcję instalacji nad którą tyle pracowaliśmy:

if systemService(options.chosenSystem):
	remove(options.chosenSystem)
	dockerInstall(options.chosenSystem)
else:
	print("Script does not support system you choose. See help -h for more.")

W ten sposób cały skrypt mamy już przygotowany i możemy przystąpić do instalacji.

Cały skrypt – podstawy języka Python w systemie Linux

Poniżej prezentuje cały skrypt:

#!/usr/bin/env python3
import subprocess
import argparse


systemList = {'centos' : 'yum', 'fedora' : 'dnf', 'rhel' : 'yum', 'sles' : 'zypper', 'debian' : 'apt', 'ubuntu' : 'apt'}

# from 0 to 7 - CentOS
# from 0 to 9 - Fedora
# from 0 to end - RHEL
# from 0 to 7 and 11 - SLES

dockerRemoveRH = ['docker', 'docker-client', 'docker-client-latest', 'docker-common', 'docker-latest', 'docker-latest-logrotate', \
'docker-logrotate', 'docker-engine', 'docker-selinux', 'docker-engine-selinux', 'podman', 'runc']

# Ubuntu and Debian

dockerRemoveDEB = ['docker', 'docker-engine', 'docker.io', 'containerd', 'runc']
redHatRepos = "sudo yum-config-manager --add-repo "
dockerInstalaction = " docker-ce docker-ce-cli containerd.io docker-compose-plugin"

def callOptions():
	system = argparse.ArgumentParser()
	system.add_argument("-s", "--system", dest="chosenSystem", help="Write a name of system witch you are using. \
		You can choose from: CentOS, Debian, Fedora, RHEL, SLES and Ubuntu")
	options = system.parse_args()
	if not options.chosenSystem:
		system.error('You haven\'t specified the system you are using!')
	return options

def systemService(systemName):
	if systemName in systemList:
		return True
	else:
		return False

def redHatRemove(systemName):
	i = 1
	removeApp = ""

	if systemName == 'centos':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['centos'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'fedora':
		while i != 10:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['fedora'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'rhel':
		while i != len(dockerRemoveRH):
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['rhel'] + " " + "remove " + removeApp + " -y"
		return remove
	elif systemName == 'sles':
		while i != 8:
			removeApp += dockerRemoveRH[i] + " "
			i += 1
		remove = 'sudo ' + systemList['sles'] + " " + "remove " + removeApp + dockerRemoveRH[11] + " -y"
		return remove

def debianRemove():
	i = 1
	removeApp = ""

	while i != len(dockerRemoveDEB):
		removeApp += dockerRemoveDEB[i] + " "
		i += 1

	remove = 'sudo ' + systemList['debian'] + " " + "remove " + removeApp + " -y"
	return remove

def remove(systemName):
	if (systemName == 'debian') or (systemName == 'ubuntu'):
		subprocess.call(debianRemove(), shell=True)
	if (systemName == 'centos') or (systemName == 'fedora') or (systemName == 'rhel'):
		subprocess.call(redHatRemove(systemName), shell=True)

def install(systemName, instalactionPackages):
	subprocess.call("sudo " + systemList[systemName] + " install " + instalactionPackages + " -y", shell=True)


def debUpdate():
	subprocess.call("sudo apt update", shell=True)

def debRepoAdd(systemName):
	subprocess.call("sudo mkdir -p /etc/apt/keyrings", shell=True)
	subprocess.call('curl -fsSL https://download.docker.com/linux/' + systemName + '/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg', shell=True)
	subprocess.call('echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/"' \
		+ systemName +'" $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null', shell=True)

def dockerInstall(systemName):
	if (systemName == 'centos') or (systemName == 'rhel'):
		install(systemName, 'yum-utils')
		subprocess.call(redHatRepos + " https://download.docker.com/linux/centos/docker-ce.repo -y", shell=True)
		install(systemName, dockerInstalaction)
	elif systemName == 'fedora':
		install(systemName, 'dnf-plugins-core')	
		subprocess.call("dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo -y", shell=True)
		install(systemName, dockerInstalaction)
	# elif systemName == 'rhel':
	# 	install(systemName, 'yum-utils')
	# 	subprocess.call(redHatRepos + " https://download.docker.com/linux/rhel/docker-ce.repo -y", shell=True)
	# 	install(systemName, dockerInstalaction)
	elif systemName == 'sles':
		pass
	elif systemName == 'debian':
		debUpdate()
		install(systemName, 'ca-certificates curl gnupg lsb-release')
		debRepoAdd('debian')
		debUpdate()
		install(systemName, dockerInstalaction)
	elif systemName == 'ubuntu':
		debUpdate()
		install(systemName, 'ca-certificates curl gnupg lsb-release')
		debRepoAdd('ubuntu')
		debUpdate()
		install(systemName, dockerInstalaction)
		
options = callOptions()

if systemService(options.chosenSystem):
	remove(options.chosenSystem)
	dockerInstall(options.chosenSystem)
else:
	print("Script does not support system you choose. See help -h for more.")

Podsumowanie – podstawy języka Python w systemie Linux

Dość dużo czasu poświeciliśmy na stworzenie takiego skryptu.  Musisz mieć świadomość tak jak ja mam, że skrypt pod względem swojej konstrukcji nie jest doskonały. Ulepszaniem go zajmiemy się w następnym artykule. Celem tego artykułu było wytłumaczenie jak największej ilości rzeczy związanych z programowaniem w języku Python. I przy tym napisanie czegoś konkretniejszego niż sama aktualizacja systemu. Dlatego jest on dość długi i zawiera sporo powtórzeń, których powinno się unikać w programowaniu. Takie są podstawy języka Python w systemie Linux. Jednak pomimo tego działa. Jak to kiedyś mawiał mój kolega, gdy uczyłem się programowania, najważniejsze że działa.

Chcesz więcej?

Oczywiście, jeżeli nie chcesz przegapić tego typu materiałów, to zapisz się na mój newsletter i przy okazji odbierz darmowe bonusy, np. e-booka ze zdjęcia poniżej! Prowadzę regularny, cotygodniowy newsletter z poradami związanymi z automatyzacją, monitoringiem oraz dobrymi praktykami w IT.

Zapisy TUTAJ

Zapisz się na Newsletter i odbierz za darmo e-book "Monitoring IT"!

X