
LXD — este un manager de containere de nouă generație, așa cum se afirmă . Oferă o interfață utilizator similară cu cea a mașinilor virtuale, dar folosește în schimb containere Linux.
Nucleul LXD — este un demon privilegiat (un serviciu care rulează cu drepturi de root) care oferă un API REST printr-un socket unix local, dar și prin rețea, dacă este configurat corespunzător. Clienți, cum ar fi instrumentul de linie de comandă furnizat cu LXD, trimit solicitări prin acest API REST. Aceasta înseamnă că, indiferent dacă accesați un gazdă locală sau una remote, totul funcționează la fel.
În acest articol nu ne vom axa pe conceptele LXD, nici nu vom analiza toate funcționalitățile disponibile prezentate în documentație, inclusiv implementarea recentă în ultimele versiuni LXD a suportului pentru mașini virtuale QEMU simultan cu containerele. În schimb, ne vom concentra doar asupra funcționalităților de bază pentru gestionarea containerelor — vom configura grupuri de stocare, rețea, vom lansa un container, vom aplica limite de resurse și vom explora cum să folosim instantanee, astfel încât să obțineți o înțelegere de bază a LXD și să utilizați containere în Linux.
Pentru informații complete, consultați sursa oficială:
Navigare
Instalarea LXD
Instalarea LXD în distribuțiile Ubuntu
În distribuția Ubuntu 19.10, pachetul lxd are o tranziție către :
apt search lxd
lxd/eoan 1:0.7 all
Pachet tranzițional - lxd -> snap (lxd)Aceasta înseamnă că vor fi instalate simultan două pachete, unul sistemic și celălalt ca pachet snap. Instalarea a două pachete în sistem poate crea o problemă, în care pachetul sistemic poate deveni orfan dacă se șterge pachetul snap cu managerul de pachete snap.
Găsiți pachetul lxd în depozitul snap folosind următoarea comandă:
snap find lxd
Name Version Summary
lxd 3.21 System container manager and API
lxd-demo-server 0+git.6d54658 Online software demo sessions using LXD
nova ocata OpenStack Compute Service (nova)
nova-hypervisor ocata OpenStack Compute Service - KVM Hypervisor (nova)
distrobuilder 1.0 Image builder for LXC and LXD
fabrica 0.1 Build snaps by simply pointing a web form to...
satellite 0.1.2 Advanced scalable Open source intelligence platformRulând comanda list poți verifica dacă pachetul lxd nu este încă instalat:
snap list
Name Version Rev Tracking Publisher Notes
core 16-2.43.3 8689 stable canonical✓ coreÎn ciuda faptului că LXD este un pachet snap, acesta trebuie instalat prin intermediul pachetului sistemic lxd, care va crea în sistem grupul corespunzător, utilitățile necesare în /usr/bin etc.
sudo apt update
sudo apt install lxdSă ne asigurăm că pachetul este instalat ca pachet snap:
snap list
Name Version Rev Tracking Publisher Notes
core 16-2.43.3 8689 stable canonical✓ core
lxd 3.21 13474 stable\/… canonical✓ -Instalarea LXD în distribuțiile Arch Linux
Pentru a instala pachetul LXD în sistem, trebuie să rulăm următoarele comenzi, prima - actualizează lista pachetelor disponibile în depozit, a doua - va instala efectiv pachetul:
sudo pacman -Syyu && sudo pacman -S lxdDupă instalarea pachetului, pentru a gestiona LXD ca utilizator obișnuit, acesta trebuie adăugat în grupul sistemic lxd:
sudo usermod -a -G lxd user1Să ne asigurăm că utilizatorul user1 a fost adăugat în grup lxd:
id -Gn user1
user1 adm dialout cdrom floppy sudo audio dip video plugdev netdev lxdDacă grupul lxd nu apare în listă, atunci este necesar să activați din nou sesiunea utilizatorului. Pentru aceasta, trebuie să vă deconectați și să vă reconectați la sistem sub același utilizator.
Activăm systemd încărcarea serviciului LXD la startul sistemului:
sudo systemctl enable lxdPornim serviciul:
sudo systemctl start lxdVerificăm statusul serviciului:
sudo systemctl status lxdStocarea LXD (Storage)
Înainte de a începe inițializarea, trebuie să înțelegem cum este organizat logic stocajul în LXD.
Stocarea (Stocare) formată din unul sau mai multe Pool de stocare care utilizează unul dintre sistemele de fișiere acceptate, cum ar fi ZFS, BTRFS, LVM sau directoare obișnuite. Fiecare Pool de stocare este împărțit în volume (Storage Volume) care conțin imagini, containere sau date pentru alte scopuri.
- Imagini — acestea sunt distribuții specializate fără nucleu Linux, disponibile din surse externe
- Containere — acestea sunt distribuții desfășurate din imagini, gata de utilizare
- Snapshot-uri — acestea sunt instantanee ale stării containerelor la care se poate reveni

Pentru gestionarea stocării în LXD se folosește comanda lxc storage informații despre care pot fi obținute specificând cheia — lxc storage --help
Următoarea comandă afișează pe ecran lista tuturor Pool de stocare în stocarea LXD:
lxc storage list
+---------+-------------+--------+--------------------------------+---------+
| NAME | DESCRIPTION | DRIVER | SOURCE | USED BY |
+---------+-------------+--------+--------------------------------+---------+
| hddpool | | btrfs | /dev/loop1 | 2 |
+---------+-------------+--------+--------------------------------+---------+
| ssdpool | | btrfs | /var/lib/lxd/disks/ssdpool.img | 4 |
+---------+-------------+--------+--------------------------------+---------+Pentru a vizualiza lista tuturor Storage Volume în Pool de stocare se folosește comanda lxc storage volume list:
lxc storage volume list hddpool
+-------+----------------------------------+-------------+---------+
| TYPE | NAME | DESCRIPTION | USED BY |
+-------+----------------------------------+-------------+---------+
| image | ebd565585223487526ddb3607f515... | | 1 |
+-------+----------------------------------+-------------+---------+lxc storage volume list ssdpool
+-----------+----------------------------------+-------------+---------+
| TYPE | NAME | DESCRIPTION | USED BY |
+-----------+----------------------------------+-------------+---------+
| container | alp3 | | 1 |
+-----------+----------------------------------+-------------+---------+
| container | jupyter | | 1 |
+-----------+----------------------------------+-------------+---------+
| image | ebd565585223487526ddb3607f515... | | 1 |
+-----------+----------------------------------+-------------+---------+De asemenea, dacă pentru Pool de stocare la crearea sa a fost ales sistemul de fișiere BTRFS, pentru a obține lista Storage Volume sau subvolume în interpretarea BTRFS se poate folosi instrumentarul acestui sistem de fișiere:
sudo btrfs subvolume list -p /var/lib/lxd/storage-pools/hddpool
ID 257 gen 818 parent 5 top level 5 path images/ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3sudo btrfs subvolume list -p /var/lib/lxd/storage-pools/ssdpool
ID 257 gen 1820 parent 5 top level 5 path images/ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3
ID 260 gen 1819 parent 5 top level 5 path containers/jupyter
ID 263 gen 1820 parent 5 top level 5 path containers/alp3Inițializarea LXD
Înainte de a crea și utiliza containere, este necesară o inițializare generală a LXD care configurează rețeaua și stocarea. Acest lucru se poate realiza manual folosind comenzile standard ale clientului care sunt disponibile în lista obținută prin apelarea comenzii lxc --help sau cu ajutorul asistentului de inițializare lxd init răspunzând la câteva întrebări.
Alegerea sistemului de fișiere pentru grupul de stocare
În timpul inițializării, LXD pune câteva întrebări, printre care se va determina tipul de sistem de fișiere pentru implicit. Pool de stocareImplicit, se alege sistemul de fișiere BTRFS. Schimbarea pentru un alt FS după creare nu va fi posibilă.Pentru a alege FS-ul, se propune :
Caracteristică
Director
Btrfs
LVM
ZFS
CEPH
Stocare de imagini optimizată
no
da
da
da
da
Creare de instanțe optimizată
no
da
da
da
da
Creare de snapshot-uri optimizată
no
da
da
da
da
Transfer de imagini optimizat
no
da
no
da
da
Transfer de instanțe optimizat
no
da
no
da
da
Copiere la scriere
no
da
da
da
da
Pe bază de blocuri
no
no
da
no
da
Clonare instantanee
no
da
da
da
da
Driver de stocare utilizabil în interiorul unui container
da
da
no
no
no
Restaurare din snapshot-uri mai vechi (nu cele mai recente)
da
da
da
no
da
Quotas de stocare
da(*)
da
da
da
no
Inițializarea rețelei și a grupului de stocare cu ajutorul asistentului
Urmați comanda pe care o vom analiza, care oferă configurarea principalelor componente LXD prin răspunsurile la întrebări simple folosind asistentul de inițializare.
Rulați comanda lxc init și introduceți răspunsurile la întrebări după semnul două puncte, așa cum este arătat în exemplul de mai jos sau modificați-le conform condițiilor dumneavoastră:
lxd init
Ați dori să utilizați clusteringul LXD? (da/nu) [implicit=nu]:
Doriți să configurați un nou pool de stocare? (da/nu) [implicit=da]:
Numele noului pool de stocare [implicit=default]: ssdpool
Numele backend-ului de stocare de utilizat (lvm, btrfs, dir) [implicit=btrfs]:
Creați un nou pool BTRFS? (da/nu) [implicit=da]:
Ați dori să utilizați un dispozitiv de bloc existent? (da/nu) [implicit=nu]:
Dimensiunea în GB a noului dispozitiv loop (minim 1GB) [implicit=15GB]: 10GB
Ați dori să vă conectați la un server MAAS? (da/nu) [implicit=nu]:
Ați dori să creați un nou bridge de rețea local? (da/nu) [implicit=da]:
Cum ar trebui să fie numit noul bridge? [implicit=lxdbr0]:
Ce adresă IPv4 ar trebui utilizată? (notare CIDR, “auto” sau “none”) [implicit=auto]: 10.0.5.1/24
Ați dori ca LXD să NAT-eze traficul IPv4 pe bridge-ul dumneavoastră? [implicit=da]:
Ce adresă IPv6 ar trebui utilizată? (notare CIDR, “auto” sau “none”) [implicit=auto]: none
Ați dori ca LXD să fie disponibil în rețea? (da/nu) [implicit=nu]:
Ați dori ca imaginile cache expirate să fie actualizate automat? (da/nu) [implicit=da] nu
Ați dori să fie tipărit un YAML "lxd init" preseed? (da/nu) [implicit=nu]: Crearea unui grup de stocare suplimentar
În pasul anterior, am creat Pool de stocare pe care i-am dat numele ssdpool iar fișierul acestuia se află în sistemul meu la adresa /var/lib/lxd/disks/ssdpool.img. Această adresă a sistemului de fișiere corespunde discului fizic SSD din PC-ul meu.
Următoarele acțiuni, pentru a extinde înțelegerea rolului pe care îl joacă Pool de stocare în stocare, vom crea un al doilea Pool de stocare care va fi amplasat fizic pe un alt tip de disc, pe HDD. Problema este că LXD nu permite crearea Pool de stocare în afara adresei /var/lib/lxd/disks/ și chiar și simbolurile legătură nu vor funcționa, . Putem ocoli această restricție în timpul inițializării/formatarea, Pool de stocare specificând valoarea ca dispozitiv de bloc în loc de calea către fișierul loopback specificând asta în cheia source.
Așadar, până la crearea Pool de stocare Este necesar să definim un fișier loopback sau o partiție existentă în sistemul nostru de fișiere pe care acesta o va folosi. Pentru aceasta, vom crea și vom folosi un fișier căruia îi vom limita dimensiunea la 10GB:
dd if=/dev/zero of=/mnt/work/lxd/hddpool.img bs=1MB count=10000
10000+0 registre în
10000+0 registre ies
10000000000 octeți (10 GB, 9,3 GiB) copiați, 38,4414 s, 260 MB/sVom conecta fișierul loopback la un dispozitiv loopback liber:
sudo losetup --find --show /mnt/work/lxd/hddpool.img
/dev/loop1Datorită opțiunii --show executarea comenzii returnează pe ecran numele dispozitivului la care s-a conectat fișierul nostru loopback. Dacă este necesar, putem afișa pe ecran lista tuturor dispozitivelor ocupate de acest tip, pentru a ne asigura de corectitudinea acțiunilor noastre:
losetup -l
NAME SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE DIO LOG-SEC
/dev/loop1 0 0 0 0 /mnt/work/lxd/hddpool.img 0 512
/dev/loop0 0 0 1 0 /var/lib/lxd/disks/ssdpool.img 0 512Din listă putem observa că pe dispozitivul /dev/loop1 este conectat un fișier loopback /mnt/work/lxd/hddpool.img, iar pe dispozitivul /dev/loop0 este conectat un fișier loopback /var/lib/lxd/disks/ssdpool.img care corespunde celui implicit Pool de stocare.
Următoarea comandă creează un nou Pool de stocare în LXD, pe baza fișierului loopback tocmai pregătit. LXD va formata fișierul loopback /mnt/work/lxd/hddpool.img pe dispozitivul /dev/loop1 în sistemul de fișiere BTRFS:
lxc storage create hddpool btrfs size=10GB source=/dev/loop1Să afișăm lista tuturor Pool de stocare pe ecran:
lxc storage list
+---------+-------------+--------+--------------------------------+---------+
| NAME | DESCRIPTION | DRIVER | SOURCE | USED BY |
+---------+-------------+--------+--------------------------------+---------+
| hddpool | | btrfs | /dev/loop1 | 0 |
+---------+-------------+--------+--------------------------------+---------+
| ssdpool | | btrfs | /var/lib/lxd/disks/ssdpool.img | 0 |
+---------+-------------+--------+--------------------------------+---------+Creșterea dimensiunii grupului de stocare
După crearea Pool de stocare, dacă este necesar, acesta poate fi extins. Pentru Pool de stocare bazat pe sistemul de fișiere BTRFS, executați următoarele comenzi:
sudo truncate -s +5G /mnt/work/lxd/hddpool.img
sudo losetup -c /dev/loop1
sudo btrfs filesystem resize max /var/lib/lxd/storage-pools/hddpoolInserarea automată a fișierului loopback în slotul dispozitivului loopback
Avem o mică problemă, la repornirea sistemului gazdă, fișierul /mnt/work/lxd/hddpool.img "va " ieși din dispozitiv /dev/loop1 și serviciul LXD va cădea la pornire deoarece nu îl va găsi în acest dispozitiv. Pentru a rezolva această problemă, trebuie să creăm un serviciu de sistem care va conecta acest fișier la dispozitiv /dev/loop1 la fiecare pornire a sistemului gazdă.
Să creăm unit un fișier de tip service în /etc/systemd/system/ pentru sistemul de inițializare SystemD:
cat << EOF | sudo tee -a /etc/systemd/system/lxd-hddpool.service
[Unit]
Description=Losetup LXD Storage Pool (hddpool)
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/sbin/losetup /dev/loop1 /mnt/work/lxd/hddpool.img
RemainAfterExit=true
[Install]
WantedBy=local-fs.target
EOFActivăm serviciul:
sudo systemctl enable lxd-hddpool
Creat un link simbolic în /etc/systemd/system/local-fs.target.wants/lxd-hddpool.service → /etc/systemd/system/lxd-hddpool.service.După restartarea sistemului gazdă, verificăm starea serviciului:
systemctl status lxd-hddpool.service
● lxd-hddpool.service - Losetup LXD Storage Pool (hddpool)
Încărcat: încărcat (/etc/systemd/system/lxd-hddpool.service; activat; presetare furnizor: dezactivat)
Activ: activ (ieșit) din Mie 2020-04-08 03:43:53 MSK; acum 1 minut 37 secunde
Proces: 711 ExecStart=/sbin/losetup /dev/loop1 /mnt/work/lxd/hddpool.img (cod=ieșit, statut=0/SUCCES)
PID principal: 711 (cod=ieșit, statut=0/SUCCES)
apr 08 03:43:52 manjaro systemd[1]: Pornind Losetup LXD Storage Pool (hddpool)...
apr 08 03:43:53 manjaro systemd[1]: Terminând Losetup LXD Storage Pool (hddpool).Din ieșire putem confirma că starea serviciului este activ, cu toate că executarea scriptului nostru dintr-o singură comandă a ieșit, acest lucru ne-a permis opțiunea RemainAfterExit=true.
Securitate. Privilegii ale containerelor
Dat fiind că toate procesele containerului sunt efectiv executate în izolare pe sistemul gazdă folosind nucleul său, LXD oferă privilegii pentru procesele containerului pentru a proteja accesul acestora la sistemul gazdă, unde:
Containere privilegiate — sunt containere în care procesele cu UID și GID corespund aceluiași proprietar ca și pe sistemul gazdă. De exemplu, un proces pornit în container cu UID egal cu 0 are toate aceleași drepturi de acces ca și un proces pe sistemul gazdă cu UID egal cu 0. Cu alte cuvinte, utilizatorul root din container are toate drepturile nu doar în container, ci și pe sistemul gazdă dacă reușește să iasă din spațiul de nume izolat al containerului.
Containere nepregătite — sunt containere în care procesele aparțin unui proprietar UID și GID cu numărul de la 0 la 65535, dar pentru sistemul gazdă proprietarul este mascat prin utilizarea bitului SubUID și SubGID. De exemplu, un utilizator cu UID=0 în container va fi observat pe sistemul gazdă ca
SubUID + UID. Acest lucru protejează sistemul gazdă, deoarece, dacă vreun proces din container reușește să iasă din spațiul său de izolare, poate interacționa cu sistemul gazdă doar ca un proces cu un UID/GID necunoscut, foarte ridicat.
Implicit, containerele nou create au statutul de nepregătite și, prin urmare, trebuie să definim SubUID și SubGID.
Vom crea două fișiere de configurare în care vom seta masca pentru SubUID și SubGID, respectiv:
sudo touch /etc{/subuid,/subgid}
sudo usermod --add-subuids 1000000-1065535 root
sudo usermod --add-subgids 1000000-1065535 rootPentru a aplica modificările, serviciul LXD trebuie să fie repornit:
sudo systemctl restart lxdCrearea unui comutator virtual de rețea
Așa cum am inițializat anterior rețeaua cu ajutorul asistentului de inițializare lxd init și am creat un dispozitiv de rețea lxdbr0, în această secțiune ne vom familiariza doar cu rețeaua din LXD și cu modul de a crea un switch virtual (pod rețea) folosind comanda clientului.
Următoarea diagramă arată cum switch-ul (podul rețea) unește gazda și containerele într-o rețea:

Containerele pot interacționa prin intermediul rețelei cu alte containere sau cu gazda pe care aceste containere sunt gestionate. Pentru aceasta, este necesar să conectăm plățile de rețea virtuale ale containerelor la switch-ul virtual. La început, vom crea switch-ul, iar interfețele de rețea ale containerului vor fi conectate în capitolele următoare, după ce va fi creat containerul în sine.
Comanda următoare creează un switch cu o subrețea 10.0.5.0/24 și o adresă IPv4 10.0.5.1/24, și activează ipv4.nat pentru ca containerele să poată accesa internetul prin gazdă prin intermediul serviciului NAT:
lxc network create lxdbr0 ipv4.address=10.0.5.1/24 ipv4.nat=true ipv6.address=noneVerificăm lista dispozitivelor de rețea disponibile LXD:
lxc network list
+--------+----------+---------+-------------+---------+
| NAME | TYPE | MANAGED | DESCRIPTION | USED BY |
+--------+----------+---------+-------------+---------+
| eno1 | physical | NO | | 0 |
+--------+----------+---------+-------------+---------+
| lxdbr0 | bridge | YES | | 0 |
+--------+----------+---------+-------------+---------+De asemenea, putem verifica crearea dispozitivului de rețea folosind un instrument standard al distribuției Linux — ip link sau ip addr:
ip addr
1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eno1: mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether bc:ee:7b:5a:6b:44 brd ff:ff:ff:ff:ff:ff
altname enp0s25
inet6 fe80::9571:11f3:6e0c:c07b/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: lxdbr0: mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether c2:38:90:df:cb:59 brd ff:ff:ff:ff:ff:ff
inet 10.0.5.1/24 scope global lxdbr0
valid_lft forever preferred_lft forever
inet6 fe80::c038:90ff:fedf:cb59/64 scope link
valid_lft forever preferred_lft forever
5: veth3ddab174@if4: mtu 1500 qdisc noqueue master lxdbr0 state UP group default qlen 1000
link/ether ca:c3:5c:1d:22:26 brd ff:ff:ff:ff:ff:ff link-netnsid 0Profil de configurare
Fiecare container din LXD are propria configurație și poate să o extindă cu ajutorul configurațiilor declarate global, care se numesc profiluri de configurare. Aplicarea profilurilor de configurare pe un container are un model de cascada, următorul exemplu ilustrează acest lucru:

În acest exemplu, în sistemul LXD au fost create trei profiluri: default, hddpool și hostfs. Toate cele trei profiluri sunt aplicate pe un container care are o configurație locală (zonă gri). Profilul default are un dispozitiv root care are parametrul pool este ssdpool, dar datorită modelului de cascada al aplicării configurației putem aplica pentru container profilul hddpool care are parametrul pool va suprascrie acest același parametru din profilul default iar containerul va obține configurația dispozitivului root cu parametrul pool egal cu hddpool, iar profilul hostfs adauga pur și simplu un dispozitiv nou în container.
Pentru a vedea lista profilurilor de configurație disponibile se folosește următoarea comandă:
lxc profile list
+---------+---------+
| NAME | USED BY |
+---------+---------+
| default | 1 |
+---------+---------+
| hddroot | 0 |
+---------+---------+
| ssdroot | 1 |
+---------+---------+Lista completă a comenzilor disponibile pentru gestionarea profilului poate fi obținută adăugând cheia --help:
lxc profile --help
Description:
Gestionați profilurile
Usage:
lxc profile [comandă]
Available Commands:
add Adăugați profiluri instanțelor
assign Atribuiți seturi de profiluri instanțelor
copy Copiați profilurile
create Creați profiluri
delete Ștergeți profilurile
device Gestionați dispozitivele instanței
edit Editați configurațiile profilului ca YAML
get Obțineți valori pentru cheile de configurație ale profilului
list Listați profilurile
remove Îndepărtați profilurile din instanțe
rename Renumiți profilurile
set Setați cheile de configurație ale profilului
show Afișați configurațiile profilului
unset Resetați cheile de configurație ale profiluluiEditarea profilului
Profilul de configurare implicit default nu are configurație de placă de rețea pentru container și toate containerele nou create nu au rețea, pentru acestea este necesară crearea dispozitivelor de rețea locale (dedicate) printr-o comandă separată, dar putem crea în profilul de configurare un dispozitiv de rețea global care va fi partajat între toate containerele care utilizează acest profil. Astfel, imediat după comanda de creare a unui nou container, acestea vor avea rețea cu acces la Internet. În acest timp, nu există restricții, putem oricând să creăm mai târziu un dispozitiv de rețea local, dacă va fi necesar.
Următoarea comandă va adăuga în profilul de configurare un dispozitiv eth0 de tip nic conectat la rețea lxdbr0:
lxc profile device add default eth0 nic network=lxdbr0 name=eth0Este important de menționat că, având în vedere că am adăugat efectiv un dispozitiv în profilul de configurare, dacă am indicat un IP static pentru dispozitiv, toate containerele care vor aplica acest profil vor împărți aceeași adresă IP. Dacă este necesar să creați un container cu o adresă IP statică dedicată, trebuie să creați o configurație de dispozitiv de rețea la nivel de container (configurare locală) cu parametrul adresei IP, și nu la nivel de profil.
Să verificăm profilul:
lxc profile show default
config: {}
description: Profil implicit LXD
devices:
eth0:
name: eth0
network: lxdbr0
type: nic
root:
path: \/
pool: ssdpool
type: disk
name: default
used_by: []În acest profil putem vedea că pentru toate containerele nou create vor fi create două dispozitive (devices):
eth0— Dispozitiv de tipnicconectat la switch (podul de rețea)lxdbr0root— Dispozitiv de tipdiskcare utilizează un pool de stocaressdpool
Crearea de noi profiluri
Pentru a folosi containerele create anterior, vom crea un profil de configurare Pool de stocare ssdroot în care vom adăuga un dispozitiv de tip cu punct de montare disk (root) utilizând un pool de stocare creat anterior / lxc profile create ssdroot lxc profile device add ssdroot root disk path=\/ pool=ssdpool Pool de stocare — ssdpool:
În mod similar, creăm un dispozitiv de tip, dar în acest caz folosește disklxc profile create hddroot lxc profile device add hddroot root disk path=\/ pool=hddpool Pool de stocare — hddpool:
Verificăm profilurile de configurare:lxc profile show ssdrootconfig: {} description: "" devices: root: path: \/ pool: ssdpool type: disk name: ssdroot used_by: []
lxc profile show hddroot
config: {}
description: ""
devices:
root:
path: \/
pool: hddpool
type: disk
name: hddroot
used_by: []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.Repository de imagini
Implicit, LXD este configurat să obțină imagini din trei surse externe:
Repository de imagini remote
ubuntu:
- (pentru imagini stabilizate Ubuntu) ubuntu-daily:
- (pentru imagini zilnice Ubuntu) (pentru un grup de alte distribuții)
- images: (pentru o mulțime de alte distribuții)
lxc remote list
+-----------------+------------------------------------------+--------+--------+
| NAME | URL | PUBLIC | STATIC |
+-----------------+------------------------------------------+--------+--------+
| images | https://images.linuxcontainers.org | DA | NU |
+-----------------+------------------------------------------+--------+--------+
| local (default) | unix:// | NU | DA |
+-----------------+------------------------------------------+--------+--------+
| ubuntu | https://cloud-images.ubuntu.com/releases | DA | DA |
+-----------------+------------------------------------------+--------+--------+
| ubuntu-daily | https://cloud-images.ubuntu.com/daily | DA | DA |
+-----------------+------------------------------------------+--------+--------+De exemplu, depozitul (pentru imagini stabilizate Ubuntu) are următoarele imagini:
lxc image -c dasut list ubuntu: | head -n 11
+----------------------------------------------+--------------+----------+------------+
| DESCRIPTION | ARCHITECTURE | SIZE | TYPE |
+----------------------------------------------+--------------+----------+------------+
| ubuntu 12.04 LTS amd64 (release) (20150728) | x86_64 | 153.72MB | CONTAINER |
+----------------------------------------------+--------------+----------+------------+
| ubuntu 12.04 LTS amd64 (release) (20150819) | x86_64 | 152.91MB | CONTAINER |
+----------------------------------------------+--------------+----------+------------+
| ubuntu 12.04 LTS amd64 (release) (20150906) | x86_64 | 154.69MB | CONTAINER |
+----------------------------------------------+--------------+----------+------------+
| ubuntu 12.04 LTS amd64 (release) (20150930) | x86_64 | 153.86MB | CONTAINER |
+----------------------------------------------+--------------+----------+------------+Pentru a afişa un număr limitat de coloane, am folosit opțiunea -c cu parametrii dasut, și am limitat de asemenea lungimea listei cu comanda head.
Pentru a obține lista imaginilor, este disponibilă filtrarea. Următoarea comandă va afișa lista tuturor arhitecturilor disponibile ale distribuției :
lxc image -c ldast list images:alpine/3.11
+------------------------------+--------------------------------------+--------------+
| ALIAS | DESCRIPTION | ARCHITECTURE |
+------------------------------+--------------------------------------+--------------+
| alpine/3.11 (3 more) | Alpine 3.11 amd64 (20200220_13:00) | x86_64 |
+------------------------------+--------------------------------------+--------------+
| alpine/3.11/arm64 (1 more) | Alpine 3.11 arm64 (20200220_13:00) | aarch64 |
+------------------------------+--------------------------------------+--------------+
| alpine/3.11/armhf (1 more) | Alpine 3.11 armhf (20200220_13:00) | armv7l |
+------------------------------+--------------------------------------+--------------+
| alpine/3.11/i386 (1 more) | Alpine 3.11 i386 (20200220_13:01) | i686 |
+------------------------------+--------------------------------------+--------------+
| alpine/3.11/ppc64el (1 more) | Alpine 3.11 ppc64el (20200220_13:00) | ppc64le |
+------------------------------+--------------------------------------+--------------+
| alpine/3.11/s390x (1 more) | Alpine 3.11 s390x (20200220_13:00) | s390x |
+------------------------------+--------------------------------------+--------------+Repository de imagini local
Pentru a începe utilizarea containerului, este necesar să adăugați o imagine dintr-un depozit global în cel local. local:. Acum depozitul local este gol, acest lucru ne va fi confirmat prin comanda lxc image list. Dacă metoda list nu specifică un depozit, atunci depozitul local va fi folosit implicit — local:
lxc image list local:
+-------+-------------+--------+-------------+--------------+------+------+
| ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE |
+-------+-------------+--------+-------------+--------------+------+------+Gestionarea imaginilor în depozit se face prin următoarele metode:
Comanda
Descriere
lxc image alias
Gestionați aliasurile imaginilor
lxc image copy
Copiați imagini între servere
lxc image delete
Ștergeți imagini
lxc image edit
Editați proprietățile imaginii
lxc image export
Exportați și descărcați imagini
lxc image import
Importați imagini în depozitul de imagini
lxc image info
Afișați informații utile despre imagini
lxc image list
Listați imagini
lxc image -token.
Actualizați imagini
lxc image afișați
Afișați proprietățile imaginii
Copiem imaginea în depozitul local din cel global images::
lxc image copy images:alpine/3.11/amd64 local: --alias=alpine3
Imagine copiată cu succes!Să afișăm lista tuturor imaginilor disponibile acum în depozitul local. local::
lxc image -c lfdatsu list local:
+---------+--------------+------------------------------------+--------------+
| ALIAS | FINGERPRINT | DESCRIPTION | ARCHITECTURE |
+---------+--------------+------------------------------------+--------------+
| alpine3 | 73a3093d4a5c | Alpine 3.11 amd64 (20200220_13:00) | x86_64 |
+---------+--------------+------------------------------------+--------------+Configurarea LXD
În plus față de modul interactiv, LXD susține de asemenea un mod neinteractiv de configurare, având în vedere că configurația este specificată sub formă de fișier YAML, un format special care permite configurarea întregii setări dintr-o dată, sărind peste execuția mai multor comenzi interactive care au fost discutate mai sus în acest articol, inclusiv configurarea rețelei, crearea profilurilor de configurare etc. Aici nu vom aborda acest subiect, vă puteți familiariza cu el singuri. .
Următoarea comandă interactivă lxc config pe care o vom discuta, permite setarea configurației. De exemplu, pentru ca imaginile descărcate în depozitul local să nu fie actualizate automat din depozitele globale, putem activa acest comportament cu următoarea comandă:
lxc config set images.auto_update_cached=falseCrearea și gestionarea unui container
Comanda utilizată pentru a crea un container este lxc init la care se transmit valorile depozit:imagine și apoi identificatorul dorit pentru container. Depozitul poate fi specificat ca local. local: și orice registru global. Dacă registrul nu este specificat, atunci, implicit, pentru căutarea imaginii se folosește registrul local. Dacă imaginea este specificată din registrul global, atunci mai întâi imaginea va fi încărcată în registrul local, iar apoi folosită pentru crearea containerului.
Să executăm următoarea comandă pentru a crea primul nostru container:
lxc init alpine3 alp --storage=hddpool --profile=default --profile=hddrootSă analizăm pe rând opțiunile comenzii pe care le folosim aici:
alpine3— Se specifică aliasul pentru imaginea care a fost anterior încărcată în registrul local. Dacă aliasul nu ar fi fost creat pentru această imagine, se poate face referire întotdeauna la imagine prin Fingerprint care este afișat în tabel.alp— Se stabilește identificatorul pentru container--storage— Această opțiune indică în ce Pool de stocare va fi creat containerul--profile— Aceste opțiuni aplică în mod cascadat configurația containerului din profilele de configurare create anterior
Lansăm containerul, care începe să inițieze sistemul init al distribuției:
lxc start alpDe asemenea, se poate folosi comanda lxc launch care permite combinarea comenzilor lxc init și lxc start într-o singură operațiune.
Verificăm starea containerului:
lxc list -c ns46tb
+------+---------+------------------+------+-----------+--------------+
| NAME | STATE | IPV4 | IPV6 | TYPE | STORAGE POOL |
+------+---------+------------------+------+-----------+--------------+
| alp | RUNNING | 10.0.5.46 (eth0) | | CONTAINER | hddpool |
+------+---------+------------------+------+-----------+--------------+Verificăm configurația containerului:
lxc config show alp
architecture: x86_64
config:
image.architecture: amd64
image.description: Alpine 3.11 amd64 (20200326_13:39)
image.os: Alpine
image.release: "3.11"
image.serial: "20200326_13:39"
image.type: squashfs
volatile.base_image: ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3
volatile.eth0.host_name: vethb1fe71d8
volatile.eth0.hwaddr: 00:16:3e:5f:73:3e
volatile.idmap.base: "0"
volatile.idmap.current: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.idmap.next: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.last_state.idmap: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.last_state.power: RUNNING
devices:
root:
path: \/
pool: hddpool
type: disk
ephemeral: false
profiles:
- default
- hddroot
stateful: false
description: ""În secțiune profiles putem să ne asigurăm că acest container folosește două profile de configurare — default și hddroot. În secțiunea devices putem descoperi doar un singur dispozitiv, deoarece dispozitivul de rețea a fost creat la nivelul profilului defaultPentru a vedea toate dispozitivele utilizate de container, trebuie să adăugăm cheia --expanded:
lxc config show alp --expanded
architecture: x86_64
config:
image.architecture: amd64
image.description: Alpine 3.11 amd64 (20200326_13:39)
image.os: Alpine
image.release: "3.11"
image.serial: "20200326_13:39"
image.type: squashfs
volatile.base_image: ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3
volatile.eth0.host_name: vethb1fe71d8
volatile.eth0.hwaddr: 00:16:3e:5f:73:3e
volatile.idmap.base: "0"
volatile.idmap.current: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.idmap.next: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.last_state.idmap: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.last_state.power: RUNNING
devices:
eth0:
name: eth0
network: lxdbr0
type: nic
root:
path: \/
pool: hddpool
type: disk
ephemeral: false
profiles:
- default
- hddroot
stateful: false
description: ""Configurarea unei adrese IP statice
Dacă încercăm să setăm o adresă IP pentru dispozitivul de rețea eth0 comanda lxc config device set alp destinată configurației containerului, vom primi o eroare care ne va informa că dispozitivul nu există, deoarece dispozitivul eth0 utilizat de container aparține profilului default:
lxc config device set alp eth0 ipv4.address 10.0.5.5
Error: The device doesn't existDesigur, putem seta o adresă IP statică pentru eth0 dispozitivul din profil, dar aceasta va fi unică pentru toate containerele care utilizează acest profil. Prin urmare, vom adăuga un dispozitiv dedicat pentru container:
lxc config device add alp eth0 nic name=eth0 nictype=bridged parent=lxdbr0 ipv4.address=10.0.5.5Apoi, este necesar să restartăm containerul:
lxc restart alpDacă acum ne uităm la configurația containerului, nu trebuie să aplicăm opțiunea --expanded pentru a vedea dispozitivul de rețea eth0, deoarece l-am creat la nivelul containerului și acesta a suprascris în cascadă același dispozitiv din profil default:
lxc config show alp
architecture: x86_64
config:
image.architecture: amd64
image.description: Alpine 3.11 amd64 (20200326_13:39)
image.os: Alpine
image.release: "3.11"
image.serial: "20200326_13:39"
image.type: squashfs
volatile.base_image: ebd565585223487526ddb3607f5156e875c15a89e21b61ef004132196da6a0a3
volatile.eth0.host_name: veth2a1dc59d
volatile.eth0.hwaddr: 00:16:3e:0e:e2:71
volatile.idmap.base: "0"
volatile.idmap.current: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.idmap.next: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.last_state.idmap: '[{"Isuid":true,"Isgid":false,"Hostid":1000000,"Nsid":0,"Maprange":65536},{"Isuid":false,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":65536}]'
volatile.last_state.power: RUNNING
devices:
eth0:
ipv4.address: 10.0.5.5
name: eth0
nictype: bridged
parent: lxdbr0
type: nic
root:
path: \
pool: hddpool
type: disk
ephemeral: false
profiles:
- default
- hddroot
stateful: false
description: ""Ștergerea unui container
Comanda pentru a elimina containerul este lxc delete, dar înainte de a elimina containerul, acesta trebuie oprit folosind comanda lxc stop:
lxc stop alplxc list
+------+---------+-------------------+------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+---------+-------------------+------+-----------+-----------+
| alp | STOPPED | 10.0.5.10 (eth0) | | CONTAINER | 0 |
+------+---------+-------------------+------+-----------+-----------+După ce ne-am asigurat că starea containerului este STOPPED, acesta poate fi eliminat din Pool de stocare:
lxc delete alpAccesul la container
Pentru a executa comenzi în container, direct, sărind peste conexiunile de rețea, există comanda lxc exec care execută comenzi în container fără a lansa un shell de sistem. Dacă trebuie să rulați o comandă în shell, folosind modele de shell, cum ar fi variabile, redirecționări de fișiere (pipe) etc., trebuie să lansați explicit shell-ul și să transmiteți comanda ca argument, de exemplu:
lxc exec alp -- /bin/sh -c "echo $HOME"În comandă a fost utilizat caracterul special de escapare pentru caracterul special $ de a nu permite variabilei $HOME să fie interpretată pe mașina gazdă, ci să fie interpretată doar în interiorul containerului.
De asemenea, este posibil să lansați un mod interactiv de shell, iar apoi să încheiați sesiunea folosind combinația de taste CTRL+D:
lxc exec alp -- /bin/shGestionarea resurselor containerului
În LXD, puteți gestiona resursele containerului folosind un set special de configurații. Lista completă a parametrilor de configurare pentru container poate fi găsită .
Limitarea resurselor RAM (memorie)
Parametru limits.memory limitează cantitatea de RAM disponibil pentru container. Valoarea specificată este un număr și unul dintre .
Să setăm o limită de RAM pentru container de 256 MB:
lxc config set alp limits.memory 256MBDe asemenea, există și alte opțiuni pentru limitarea memoriei:
limits.memory.enforcelimits.memory.hugepageslimits.memory.swaplimits.memory.swap.priority
Comanda lxc config show permite afișarea întregii configurații a containerului, inclusiv restricțiile de resurse aplicate:
lxc config show alp
architecture: x86_64
config:
image.architecture: amd64
image.description: Alpine 3.11 amd64 (20200220_13:00)
image.os: Alpine
image.release: "3.11"
image.serial: "20200220_13:00"
image.type: squashfs
limits.memory: 256MB
volatile.base_image: 73a3093d4a5ce0148fd84b95369b3fbecd19a537ddfd2e2d20caa2eef0e8fd60
volatile.eth0.host_name: veth75b6df07
volatile.eth0.hwaddr: 00:16:3e:a1:e7:46
volatile.idmap.base: "0"
volatile.idmap.current: '[]'
volatile.idmap.next: '[]'
volatile.last_state.idmap: '[]'
volatile.last_state.power: RUNNING
devices: {}
ephemeral: false
profiles:
- default
stateful: false
description: ""Limitarea resurselor CPU (procesor)
Pentru limitarea resurselor CPU există mai multe :
limit.cpu— leagă containerul de unul sau mai multe nuclee CPUlimits.cpu.allowance— gestionează fie cotele planificatorului CFS, atunci când s-a depășit limita de timp, fie un mecanism universal de partajare a resurselor CPU, atunci când s-a depășit o valoare procentualălimits.cpu.priority— prioritatea planificatorului, atunci când mai multe instanțe, care partajează un set de procesoare, au fost atribuite aceeași procentaj din procesoare
lxc config set alp limits.cpu.allowance 40%lxc config show alp
architecture: x86_64
config:
image.architecture: amd64
image.description: Alpine 3.11 amd64 (20200220_13:00)
image.os: Alpine
image.release: "3.11"
image.serial: "20200220_13:00"
image.type: squashfs
limits.cpu.allowance: 40%
limits.memory: 256MB
volatile.base_image: 73a3093d4a5ce0148fd84b95369b3fbecd19a537ddfd2e2d20caa2eef0e8fd60
volatile.eth0.host_name: veth75b6df07
volatile.eth0.hwaddr: 00:16:3e:a1:e7:46
volatile.idmap.base: "0"
volatile.idmap.current: '[]'
volatile.idmap.next: '[]'
volatile.last_state.idmap: '[]'
volatile.last_state.power: RUNNING
devices: {}
ephemeral: false
profiles:
- default
stateful: false
description: ""Limitarea spațiului pe disc
În plus față de restricții precum limits.read, limits.write putem de asemenea limita volumul de spațiu pe disc utilizat de container (funcționează doar cu ZFS sau BTRFS):
lxc config device set alp root size=2GBDupă setare, în parametrul devices.root.size putem verifica restricția setată:
lxc config show alp
...
devices:
root:
path: \/
pool: hddpool
size: 2GB
type: disk
ephemeral: false
profiles:
- default
- hddroot
stateful: false
description: ""Pentru a vizualiza cotele utilizate pe disc putem obține din comanda lxc info:
lxc info alp
...
Resources:
Processes: 5
Disk usage:
root: 1.05GB
CPU usage:
CPU usage (in seconds): 1
Memory usage:
Memory (current): 5.46MB
Network usage:
eth0:
Bytes received: 802B
Bytes sent: 1.59kB
Packets received: 4
Packets sent: 14
lo:
Bytes received: 0B
Bytes sent: 0B
Packets received: 0
Packets sent: 0Deși am setat o limită pentru dispozitivul principal al containerului la 2GB, utilitarele de sistem precum df nu vor vedea această limită. Pentru aceasta, vom efectua un mic test și vom afla cum funcționează.
Vom crea 2 containere identice într-un singur Pool de stocare (hddpool):
lxc init alpine3 alp1 --storage=hddpool --profile=default --profile=hddroot
lxc init alpine3 alp2 --storage=hddpool --profile=default --profile=hddrootlxc list
+------+---------+------------------+------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+---------+------------------+------+-----------+-----------+
| alp1 | RUNNING | 10.0.5.46 (eth0) | | CONTAINER | 0 |
+------+---------+------------------+------+-----------+-----------+
| alp2 | RUNNING | 10.0.5.30 (eth0) | | CONTAINER | 0 |
+------+---------+------------------+------+-----------+-----------+În unul dintre containere, vom crea un fișier de 1GB:
lxc exec alp1 -- dd if=\dev\urandom of=file.img bs=1M count=1000Să ne asigurăm că fișierul a fost creat:
lxc exec alp1 -- ls -lh
total 1000M
-rw-r--r-- 1 root root 1000.0M Mar 27 10:16 file.imgDacă ne uităm în al doilea container, verificând existența fișierului în același loc, nu va exista acest fișier, ceea ce este de așteptat, deoarece containerele sunt create în propriile lor Storage Volume în același Pool de stocare:
lxc exec alp2 -- ls -lh
total 0Dar să comparăm valorile pe care le oferă df în cele două containere:
lxc exec alp1 -- df -hT
Filesystem Type Size Used Available Use% Mounted on
\dev\loop1 btrfs 9.3G 1016.4M 7.8G 11% \
...lxc exec alp2 -- df -hT
Filesystem Type Size Used Available Use% Mounted on
\dev\loop1 btrfs 9.3G 1016.4M 7.8G 11% \
...Dispozitiv /dev/loop1 montat ca partiție principală este Pool de stocare pe care aceste containere o folosesc, astfel încât ele își împart volumul în două.
Statistici despre consumul de resurse
Pentru a vizualiza statisticile de utilizare a resurselor pentru un container, se poate folosi comanda:
lxc info alp
Name: alp
Location: none
Remote: unix://
Architecture: x86_64
Created: 2020/04/08 18:05 UTC
Status: Running
Type: container
Profiles: default, hddroot
Pid: 19219
Ips:
eth0: inet 10.0.5.5 veth2a1dc59d
eth0: inet6 fe80::216:3eff:fe0e:e271 veth2a1dc59d
lo: inet 127.0.0.1
lo: inet6 ::1
Resources:
Processes: 5
Disk usage:
root: 495.62kB
CPU usage:
CPU usage (in seconds): 1
Memory usage:
Memory (current): 4.79MB
Network usage:
eth0:
Bytes received: 730B
Bytes sent: 1.59kB
Packets received: 3
Packets sent: 14
lo:
Bytes received: 0B
Bytes sent: 0B
Packets received: 0
Packets sent: 0Lucrul cu instantanee
În LXD există posibilitatea de a crea instantanee și de a restaura starea containerului din acestea.
Pentru a crea o instantanee, executați următoarea comandă:
lxc snapshot alp snapshot1Comanda lxc snapshot nu are o opțiune list, așa că, pentru a vizualiza lista de instantanee, trebuie să folosiți comanda care oferă informații generale despre container:
lxc info alp
...
...
Snapshots:
snapshot1 (taken at 2020/04/08 18:18 UTC) (stateless)Restaurarea unui container dintr-un snapshot se poate face cu comanda lxc restore indicând containerul pentru care se va efectua restaurarea și aliasul snapshot-ului:
lxc restore alp snapshot1Comanda următoare este utilizată pentru a șterge snapshot-ul. Rețineți că sintaxa comenzii este diferită față de toate celelalte, trebuie să indicați o bară oblică directă după numele containerului. Dacă bara oblică este omisă, comanda de ștergere a snapshot-ului va fi interpretată ca o comandă de ștergere a containerului!
lxc delete alp/snapshot1În exemplul de mai sus, am discutat despre așa-numitele snapshot-uri stateless. În LXD există și un alt tip de snapshot-uri — stateful, în care se păstrează starea curentă a tuturor proceselor din container. Snapshot-urile stateful sunt însoțite de o serie de funcții interesante și utile.
Ce altceva?
- Pentru dezvoltatorii Python este disponibil modulul care oferă API pentru LXD
UPDATE 10.04.2020 15:00: Am adăugat navigația
Sursa: habr.com
