Ați experimentat vreodată cu codul sau utilitarele de sistem în Linux astfel încât să nu vă faceți griji pentru sistemul de bază și să nu ștergeți totul în cazul unei erori a codului care ar trebui să ruleze cu privilegii de root?
Și ce părere aveți despre cazul în care ar fi necesar să testați sau să rulați un întreg cluster de microservicii pe o singură mașină? O sută sau chiar o mie?
Cu mașinile virtuale gestionate de un hipervizor, aceste sarcini pot fi soluționate, dar la ce preț? De exemplu, un container LXD bazat pe distribuția Alpine Linux consumă minim 7.60MB RAM, și unde partiția rădăcină după pornire ocupă 9.5MB! Ce părere ai, Elon Musk? Îți recomand să te familiarizezi cu posibilitățile de bază ale LXD - sistemul de containere din Linux
După ce a devenit clar ce sunt containerele LXD, haideți să mergem mai departe și să ne gândim, ce ar fi dacă ar exista o platformă-combinat unde am putea rula în siguranță cod pentru gazdă, genera grafice, lega dinamic (interactiv) widget-uri UI de codul tău, adăuga text codului cu blackjack… și formatare? Ceva de genul unui blog interactiv? Wow… Vreau! Vreau! 🙂
Aruncă o privire sub cortina unde vom rula într-un container JupyterLab — următoarea generație a interfeței utilizatorului în locul vechiului Jupyter Notebook, și vom instala module Python precum NumPy, Pandas, Matplotlib, IPyWidgets care vor permite să facem tot ce am menționat mai sus și să salvăm totul într-un fișier special - un notebook IPython.

Navigare
- Planul de lansare în orbită
- Instalarea și configurarea sistemului de bază
- Instalarea și configurarea software-ului de bază și a sistemului
- Instalarea și configurarea JupyterLab
- Împărțim datele cu gazda
- Hello, World!
- Extindem capacitățile Python
- Testăm modulele în JupyterLab
- Ce altceva?
Planul de lansare în orbită ^

Să facem un plan scurt de acțiune pentru a ne facilita implementarea schemei de mai sus:
- Vom instala și rula un container bazat pe distribuția Alpine Linux. Vom folosi această distribuție deoarece este orientată spre minimalism și vom instala în ea doar software-ul necesar, nimic în plus.
- Vom adăuga un disc virtual suplimentar în container, căruia îi vom da un nume —
hostfsși vom monta la sistemul de fișiere rădăcină. Acest disc va oferi posibilitatea de a utiliza fișierele gazdelor dintr-un director specificat în interiorul containerului. Astfel, datele vor fi independente de container. În cazul ștergerii containerului, datele vor rămâne pe gazdă. Această schemă este, de asemenea, utilă pentru a împărți datele între mai multe containere fără a folosi mecanismele de rețea din distribuția containerului. - Vom instala Bash, sudo, bibliotecile necesare, vom adăuga și configura utilizatorul de sistem
- Vom instala Python, module și vom compila pentru ele dependențele binare
- Vom instala și vom rula JupyterLab, vom configura aspectul, vom instala extensiile pentru acesta.
În acest articol, vom începe cu pornirea containerului, nu vom discuta despre instalarea și configurarea LXD, toate acestea le puteți găsi într-un alt articol — Funcțiile de bază LXD - sisteme de containere în Linux.
Instalarea și configurarea sistemului de bază ^
Creăm un container cu comanda în care specificăm imaginea — alpine3, identificatorul pentru container — jupyterlab și, dacă este necesar, profilele de configurare:
lxc init alpine3 jupyterlab --profile=default --profile=hddrootAici folosesc profilul de configurare hddroot care indică să creez un container cu o partiție root în Pool de stocare localizată pe un disc fizic HDD:
Containerele sunt create din imagini care sunt distribuții special concepute, fără nucleu Linux. De aceea, înainte de a lansa un container, acesta trebuie să fie desfășurat din această imagine. Sursa imaginilor este un depozit local în care imaginile sunt descărcate din depozite externe.lxc storage show hddpool
config:
size: 10GB
source: /dev/loop1
volatile.initial_source: /dev/loop1
description: ""
name: hddpool
driver: btrfs
used_by:
- /1.0/images/ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3
- /1.0/profiles/hddroot
status: Created
locations:
- noneAceasta îmi oferă posibilitatea de a experimenta cu containere pe disc HDD, economisind resursele discului SSD care este, de asemenea, prezent în sistemul meu 🙂 pentru care am creat un profil de configurare separat în care vom adăuga un dispozitiv de tip.
După crearea containerului, acesta se află în starea STOPPED, așadar trebuie să-l pornim, lansând în el sistemul init:
lxc start jupyterlabSă afișăm lista containerelor în LXD folosind cheia -c care indică ce ccoloane să apară pe ecran:
lxc list -c ns4b
+------------+---------+-------------------+--------------+
| NAME | STATE | IPV4 | STORAGE POOL |
+------------+---------+-------------------+--------------+
| jupyterlab | RUNNING | 10.0.5.198 (eth0) | hddpool |
+------------+---------+-------------------+--------------+Când am creat containerul, adresa IP a fost aleasă aleatoriu, deoarece am folosit profilul de configurare default care a fost configurat anterior în articol Funcțiile de bază LXD - sisteme de containere în Linux.
Vom schimba această adresă IP cu una mai memorabilă, creând o interfață de rețea la nivelul containerului, nu la nivelul profilului de configurare, ca în configurația actuală. Nu este obligatoriu să faceți acest lucru, puteți sări peste aceasta.
Creăm o interfață de rețea eth0 pe care o legăm cu comutatorul (bridge de rețea) lxdbr0 în care am activat NAT conform articolului anterior și acum containerul va avea acces la Internet, iar interfeței îi vom atribui o adresă IP statică — 10.0.5.5:
lxc config device add jupyterlab eth0 nic name=eth0 nictype=bridged parent=lxdbr0 ipv4.address=10.0.5.5După ce am adăugat dispozitivul, containerul trebuie repornit:
lxc restart jupyterlabVerificăm statusul containerului:
lxc list -c ns4b
+------------+---------+------------------+--------------+
| NAME | STATE | IPV4 | STORAGE POOL |
+------------+---------+------------------+--------------+
| jupyterlab | RUNNING | 10.0.5.5 (eth0) | hddpool |
+------------+---------+------------------+--------------+Instalarea și configurarea software-ului de bază și a sistemului ^
Pentru administrarea containerului nostru, trebuie să instalăm următorul software:
Package
Description
bash
The GNU Bourne Again shell
bash-completion
Finalizarea programabilă pentru shell-ul bash
sudo
Acordarea anumitor utilizatori capacitatea de a rula comenzi ca root
shadow
Suită de instrumente pentru gestionarea parolelor și conturilor care suportă fișiere shadow și PAM
tzdata
Surse pentru informațiile despre fusuri orare și ora de vară
nano
Clone de editor Pico cu îmbunătățiri
În plus, puteți instala suport pentru man-pages instalând următoarele pachete — man man-pages mdocml-apropos less
lxc exec jupyterlab -- apk add bash bash-completion sudo shadow tzdata nanoSă analizăm comenzile și opțiunile pe care le-am folosit:
lxc— Apelul clientului LXDexec— Metoda clientului LXD, care rulează comanda în containerjupyterlab— Identificatorul containerului--— O opțiune specială care indică să nu se interpreteze mai departe opțiunile ca opțiuni pentrulxcși să se transmită întreaga linie rămasă ca atare în containerapk— Managerul de pachete al distribuției Alpine Linuxadd— Metoda managerului de pachete care instalează pachetele specificate după comandă
Apoi, vom seta fusul orar în sistem Europe/Moscow:
lxc exec jupyterlab -- cp /usr/share/zoneinfo/Europe/Moscow /etc/localtimeDupă ce am instalat fusul orar, pachetul tzdata nu mai este necesar în sistem, va ocupa spațiu, așa că să-l eliminăm:
lxc exec jupyterlab -- apk del tzdataVerificăm fusul orar:
lxc exec jupyterlab -- date
Wed Apr 15 10:49:56 MSK 2020Pentru a nu pierde mult timp configurând Bash pentru noii utilizatori din container, următoarele acțiuni, vom copia din sistemul gazdă fișierele skel care sunt deja gata. Aceasta va permite să embeliți Bash în container în modul interactiv. Sistemul meu gazdă este Manjaro Linux, iar fișierele copiate /etc/skel/.bash_profile, /etc/skel/.bashrc, /etc/skel/.dir_colors se potrivesc în principiu cu Alpine Linux și nu cauzează probleme critice, dar ar putea fi un alt distribuitor și va trebui să vă descurcați singuri în cazul unei erori la pornirea Bash în container.
Copiem fișierele skel în container. Opțiunea --create-dirs va crea directoarele necesare, dacă acestea nu există:
lxc file push /etc/skel/.bash_profile jupyterlab/etc/skel/.bash_profile --create-dirs
lxc file push /etc/skel/.bashrc jupyterlab/etc/skel/.bashrc
lxc file push /etc/skel/.dir_colors jupyterlab/etc/skel/.dir_colorsPentru utilizatorul root deja existent, vom copia în directorul său Home fișierele skel tocmai copiate în container:
lxc exec jupyterlab -- cp /etc/skel/.bash_profile /root/.bash_profile
lxc exec jupyterlab -- cp /etc/skel/.bashrc /root/.bashrc
lxc exec jupyterlab -- cp /etc/skel/.dir_colors /root/.dir_colorsÎn Alpine Linux, shell-ul de sistem este setat pentru utilizatori /bin/sh, vom schimba acesta la root utilizator pentru Bash:
lxc exec jupyterlab -- usermod --shell=/bin/bash rootPentru a root utilizatorul nu a fost fără parolă, trebuie să îi setăm o parolă. Următoarea comandă va genera și va seta o nouă parolă aleatoare pe care o veți vedea pe ecranul consolei după executarea sa:
lxc exec jupyterlab -- /bin/bash -c "PASSWD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12); echo "root:$PASSWD" | chpasswd && echo "New Password: $PASSWD""
New Password: sFiXEvBswuWADe asemenea, vom crea un nou utilizator de sistem — jupyter pentru care vom configura ulterior JupyterLab:
lxc exec jupyterlab -- useradd --create-home --shell=/bin/bash jupyterVom genera și îi vom seta o parolă:
lxc exec jupyterlab -- /bin/bash -c "PASSWD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12); echo "jupyter:$PASSWD" | chpasswd && echo "New Password: $PASSWD""
New Password: ZIcbzWrF8tkiApoi, vom executa două comenzi, prima va crea un grup de sistem sudo, iar a doua va adăuga utilizatorul în acesta jupyter:
lxc exec jupyterlab -- groupadd --system sudo
lxc exec jupyterlab -- groupmems --group sudo --add jupyterSă vedem în ce grupuri este utilizatorul jupyter:
lxc exec jupyterlab -- id -Gn jupyter
jupyter sudoTotul - bine, mergem mai departe.
Vom permite tuturor utilizatorilor care sunt în grupul sudo să folosească comanda sudo. Pentru asta, executați următorul script, care sed decommentază linia parametrului din fișierul de configurare /etc/sudoers:
lxc exec jupyterlab -- /bin/bash -c "sed --in-place -e '/^#[ t]*%sudo[ t]*ALL=(ALL)[ t]*ALL$/ s/^[# ]*//' /etc/sudoers"Instalarea și configurarea JupyterLab ^
JupyterLab — este o aplicație Python, prin urmare, trebuie să instalăm întâi acest interpret. De asemenea, JupyterLab vom face instalarea cu ajutorul managerului de pachete Python pip, nu al celui sistemic, deoarece în depozitul sistemic poate fi învechit, și prin urmare, trebuie să permitem manual dependențele pentru acesta instalând următoarele pachete — python3 python3-dev gcc libc-dev zeromq-dev:
lxc exec jupyterlab -- apk add python3 python3-dev gcc libc-dev zeromq-devVom actualiza modulele Python și managerul de pachete pip la versiunea actuală:
lxc exec jupyterlab -- python3 -m pip install --upgrade pip setuptools wheelInstalăm JupyterLab prin managerul de pachete pip:
lxc exec jupyterlab -- python3 -m pip install jupyterlabDeoarece extensiile în JupyterLab sunt experimentale și oficial nu sunt livrate împreună cu pachetul jupyterlab, așa că, trebuie să le instalăm și să le configurăm manual.
Vom instala NodeJS și managerul de pachete pentru acesta — NPM, deoarece JupyterLab le folosește pentru extensiile sale:
lxc exec jupyterlab -- apk add nodejs npmPentru ca extensiile pentru JupyterLab pe care le vom instala să funcționeze, trebuie să le instalăm în directorul utilizatorului deoarece aplicația va fi lansată de utilizator. jupyterProblema este că nu există un parametru în comanda de pornire la care să putem transmite directorul, aplicația percepe doar variabila de mediu și, prin urmare, trebuie să o definim. Pentru aceasta, vom scrie comanda de export a variabilei JUPYTERLAB_DIR în mediu utilizatorului jupyter, în fișierul .bashrc, care se execută de fiecare dată când utilizatorul se conectează la sistem:
lxc exec jupyterlab -- su -l jupyter -c "echo -e "nexport JUPYTERLAB_DIR=$HOME/.local/share/jupyter/lab" >> .bashrc"Următoarea comandă va instala o extensie specială — managerul de extensii în JupyterLab:
lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=$HOME/.local/share/jupyter/lab; jupyter labextension install --no-build @jupyter-widgets/jupyterlab-manager"Acum totul este pregătit pentru prima lansare JupyterLab, dar putem instala și câteva extensii utile:
toc— Table of Contents, generează o listă de titluri în document/notebookjupyterlab-horizon-theme— Temă de interfață UIjupyterlab_neon_theme— Temă de interfață UIjupyterlab-ubu-theme— O altă temă de la autorul acestui articol 🙂 Dar în acest caz, va fi arătată instalarea din repo-ul GitHub
Așadar, executați succesiune următoarele comenzi pentru a instala aceste extensii:
lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=$HOME/.local/share/jupyter/lab; jupyter labextension install --no-build @jupyterlab/toc @mohirio/jupyterlab-horizon-theme @yeebc/jupyterlab_neon_theme"lxc exec jupyterlab -- su -l jupyter -c "wget -c https://github.com/microcoder/jupyterlab-ubu-theme/archive/master.zip"lxc exec jupyterlab -- su -l jupyter -c "unzip -q master.zip && rm master.zip"lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=$HOME/.local/share/jupyter/lab; jupyter labextension install --no-build jupyterlab-ubu-theme-master"lxc exec jupyterlab -- su -l jupyter -c "rm -r jupyterlab-ubu-theme-master"După instalarea extensiilor, trebuie să le compilăm, deoarece anterior, la instalare am specificat cheia --no-build pentru a economisi timp. Acum ne vom accelera considerabil compilându-le împreună dintr-o dată:
lxc exec jupyterlab -- su -l jupyter -c "export JUPYTERLAB_DIR=$HOME/.local/share/jupyter/lab; jupyter lab build"Acum executați următoarele două comenzi pentru prima lansare JupyterLab. Ar fi fost posibil să-l lansăm cu o singură comandă, dar în acest caz, comanda de lansare, pe care trebuie să o memorezi, va fi reținută de bash în container, nu pe gazdă, unde oricum există destule comenzi care să fie scrise în istorie 🙂
Ne conectăm în container ca utilizator jupyter:
lxc exec jupyterlab -- su -l jupyterApoi, porniți JupyterLab cu parametrii și opțiunile specificate:
[jupyter@jupyterlab ~]$ jupyter lab --ip=0.0.0.0 --no-browserAccesați în browser la adresa http://10.0.5.5:8888 și pe pagina deschisă, introduceți token accesul pe care îl veți vedea în consolă. Copiați-l și lipiți-l pe pagină, apoi faceți clic pe Autentificare. După autentificare, mergeți în partea stângă a meniului extensiilor, așa cum se arată în imaginea de mai jos, unde vi se va oferi, la activarea managerului de extensii, să acceptați riscurile de securitate asociate cu instalarea extensiilor din surse terțe pentru care echipa dezvoltare JupyterLab nu își asumă responsabilitatea:

Cu toate acestea, tocmai de aceea izolăm complet JupyterLab și îl plasăm într-un container, astfel încât extensiile terțe care necesită și folosesc NodeJS să nu poată, cel puțin, să fure datele de pe disc, în afară de cele pe care le deschidem în interiorul containerului. Accesul la documentele voastre private pe gazdă /home procesele din container este puțin probabil să reușească, iar dacă reușesc, va necesita privilegii asupra fișierelor din sistemul gazdă, deoarece rulăm containerul în mod neprivilegiat. Pe baza acestor informații, puteți evalua riscul activării extensiilor în JupyterLab.
Notebook-urile IPython create (pagini în JupyterLab) vor fi acum create în directorul home al utilizatorului — /home/jupyter, dar în planurile noastre este să partajăm datele (să le împrăștiem) între gazdă și container, așa că reveniți în consolă și opriți JupyterLab executând combinația de taste — CTRL+C și răspunzând Stabiliți o parolă și păstrați-o în siguranță! la cerere. Apoi, terminați sesiunea interactivă a utilizatorului jupyter executând combinația de taste CTRL+D.
Împărțim datele cu gazda ^
Pentru a partaja datele cu gazda, trebuie să creați în container un dispozitiv care permite acest lucru și pentru aceasta executați următoarea comandă, unde specificăm următoarele chei:
lxc config device add— Comanda adaugă configurația dispozitivuluijupyter— Identificatorul containerului în care se adaugă configurațiahostfs— Identificatorul dispozitivului. Puteți să îi atribuiți orice nume.disk— Se indică tipul dispozitivuluicale— Se indică calea din container la care LXD va monta acest dispozitivsource— Se specifică sursa, calea către directorul de pe gazdă pe care doriți să îl partajați cu containerul. Indicați calea conform preferințelor dumneavoastră.
lxc config device add jupyterlab hostfs disk path=/mnt/hostfs source=/home/dv/projects/ipython-notebooksPentru director /home/dv/projects/ipython-notebooks trebuie să fie setată permisiunea utilizatorului containerului care are acum UID egal cu SubUID + UID, consultați capitolul Securitate. Permisiunile containerelor în articolul Funcțiile de bază LXD - sisteme de containere în Linux.
Setăm permisiunea pe gazdă, unde proprietarul va fi utilizatorul containerului jupyter, iar variabila $USER va indica utilizatorul dumneavoastră de pe gazdă ca grup:
sudo chown 1001000:$USER /home/dv/projects/ipython-notebooksHello, World! ^
Dacă aveți încă o sesiune de consolă deschisă în container cu JupyterLab, atunci reporniți-o cu cheia nouă --notebook-dir setând valoarea /mnt/hostfs ca calea către rădăcina notelor în container pentru dispozitivul pe care l-am creat în pasul anterior:
jupyter lab --ip=0.0.0.0 --no-browser --notebook-dir=/mnt/hostfsApoi accesați pagina http://10.0.5.5:8888 și creați prima dvs. notă apăsând butonul de pe pagină așa cum este indicat în imaginea de mai jos:

Apoi, în câmpul de pe pagină, introduceți codul în limbajul Python care va genera clasicul Hello World!. La finalizarea introducerii, apăsați CTRL+ENTER sau butonul „play” de pe bara de instrumente de sus pentru a executa acest lucru în JupyterLab:

Asta este aproape tot pentru utilizare, dar nu va fi interesant dacă nu instalăm module Python suplimentare (aplicații complete) care permit extinderea semnificativă a capabilităților standard Python în JupyterLab, așa că, să continuăm 🙂
P.S. Este interesant că vechea implementare Jupyter sub numele de cod Jupyter Notebook nu a dispărut și aceasta există paralel cu JupyterLab. Pentru a trece la versiunea veche, accesați linkul adăugând sufixul/tree, iar trecerea la noua versiune se face cu sufixul /lab, dar nu este obligatoriu să-l specificați:
- Jupyter Notebook — http://10.0.5.5:8888/tree
- Jupyter Lab — http://10.0.5.5:8888/lab
Extindem capacitățile Python ^
În această secțiune, vom instala modulele puternice ale limbajului Python, cum ar fi NumPy, Pandas, Matplotlib, IPyWidgets rezultatele muncii cărora se integrează în note. JupyterLab.
Înainte de a instala modulele evidențiate prin managerul de pachete pip trebuie să permitem mai întâi dependențele de sistem în Alpine Linux:
g++— Necesare pentru compunerea modulelor, deoarece unele dintre ele sunt implementate în limbajul C++ și se conectează la Python în momentul execuției ca module binare.freetype-dev— dependență pentru modulul Python Matplotlib
Instalăm dependențele:
lxc exec jupyterlab -- apk add g++ freetype-devExistă o problemă: în starea actuală a distribuției Alpine Linux, nu este posibil să compilăm o nouă versiune NumPy, va apărea o eroare de compilare pe care nu am reușit să o rezolv:
EROARE: Nu s-au putut construi roțile pentru numpy, care folosesc PEP 517 și nu pot fi instalate direct
Prin urmare, acest modul îl vom instala ca pachet de sistem care distribuie deja o versiune compilată, dar puțin mai veche decât cea disponibilă acum pe site:
lxc exec jupyterlab -- apk add py3-numpy py3-numpy-devApoi, instalăm modulele Python prin managerul de pachete pip. Aveți răbdare, deoarece unele module vor fi compilate și acest lucru va dura câteva minute. Pe masina mea, compilarea a durat aproximativ 15 minute:
lxc exec jupyterlab -- python3 -m pip install pandas ipywidgets matplotlibCurățăm cache-urile instalărilor:
lxc exec jupyterlab -- rm -rf /home/*/.cache/pip/*
lxc exec jupyterlab -- rm -rf /root/.cache/pip/*Testăm modulele în JupyterLab ^
Dacă aveți JupyterLab, reporniți-l pentru a activa modulele nou instalate. Pentru aceasta, în sesiunea consolei apăsați CTRL+C unde este pornit și introduceți Stabiliți o parolă și păstrați-o în siguranță! la cererea de oprire, apoi reporniți JupyterLab apăsând săgeata de sus a tastaturii, astfel încât să nu fie nevoie să reintroduceți comanda și apoi Enter pentru a porni:
jupyter lab --ip=0.0.0.0 --no-browser --notebook-dir=/mnt/hostfsAccesați pagina http://10.0.5.5:8888/lab sau reîncărcați pagina în browser, apoi introduceți următorul cod într-o nouă celulă a notebook-ului:
%matplotlib inline
from ipywidgets import interactive
import matplotlib.pyplot as plt
import numpy as np
def f(m, b):
plt.figure(2)
x = np.linspace(-10, 10, num=1000)
plt.plot(x, m * x + b)
plt.ylim(-5, 5)
plt.show()
interactive_plot = interactive(f, m=(-2.0, 2.0), b=(-3, 3, 0.5))
output = interactive_plot.children[-1]
output.layout.height = '350px'
interactive_plotAr trebui să obțineți un rezultat similar cu imaginea de mai jos, unde IPyWidgets generează un element UI pe pagina care interacționează in mod interactiv cu codul sursă, precum și Matplotlib afişează rezultatul codului sub formă de grafice ale funcției:

Multe exemple IPyWidgets le puteți găsi în tutoriale aici
Ce altceva? ^
Felicitări că ați rămas și ați ajuns până la sfârșitul articolului. Am decis să nu public un script gata la sfârșitul articolului care să instaleze JupyterLab în „un singur clic”, pentru a încuraja muncitorii 🙂 Dar puteți face acest lucru singuri, deoarece deja știți cum, adunând comenzile într-un singur script Bash 🙂
De asemenea, puteți:
- Să atribuiți un nume de rețea pentru container în locul adresei IP scriindu-l într-un simplu
/etc/hostsși în browser să introduceți adresa http://jupyter.local:8888 - Să experimentați cu restricțiile de resurse pentru container, pentru aceasta citiți capitolul din posibilitățile de bază ale LXD sau obțineți mai multe informații pe site-ul dezvoltatorului LXD.
- Schimbați tema de design:

Și multe alte lucruri pe care le puteți face! Asta e tot. Vă doresc succes!
UPDATE: 15.04.2020 18:30 — Am corectat erorile din capitolul „Salut, lume!”
UPDATE: 16.04.2020 10:00 — Am corectat și adăugat text în descrierea activării managerului de extensii JupyterLab
UPDATE: 16.04.2020 10:40 — Am corectat erorile găsite în text și am modificat puțin, îmbunătățind capitolul „Instalarea software-ului de bază și configurarea sistemului”
Sursa: habr.com
