Laboratorul 05. Rootfs & Servicii Web

Root file system

Pentru ca sistemul să fie inițializat corect după pornirea kernel-ului, este necesar ca toate script-urile și executabilele necesare pentru a porni daemon-ul de inițializare, init, și restul proceselor user-space, să existe în anumite locații în sistemul de fișiere. Acest sistem minimal de fișiere necesar la inițializare poartă numele de root file system sau rootfs. În cazul sistemelor Unix, rootfs-ul are ca rădăcină directorul / și înglobează o serie de directoare ce contin restul de fișiere necesare. De obicei, în directorul / nu se află niciun fișier, doar subdirectoare. Aceste subdirectoare sunt organizate în funcție de fișierele pe care le conțin:

  • /bin - conține programe și comenzi necesare la inițializare ce pot fi folosite apoi de către un utilizator neprivilegiat în timpul unei sesiuni
  • /sbin - conține programe asemănătoare cu cele din /bin ca utilitate, dar care pot fi utilizate în general doar de utilizatorul privilegiat root
  • /etc - conține fișierele de configurare specifice sistemului
  • /lib - conține bibliotecile folosite de programele din rootfs (cele din /bin și /sbin)
  • /dev - conține referințe la toate dispozitivele periferice; aceste referințe sunt în general fișiere speciale
  • /boot - conține fișierele necesare bootării și imaginea kernel-ului; este posibil ca acestea să fie păstrate pe un sistem de fișiere separat de rootfs
  • /tmp - conține fișiere temporare și este de obicei curățat la repornirea sistemului
  • /opt - conține în principal aplicațiile third-party
  • /proc, /usr, /mnt, /var, /home - sunt directoare care, în general, reprezintă puncte în care se pot monta alte sisteme de fișiere pentru a stoca log-uri, biblioteci și alte aplicații și programe folosite de către utilizatori.

Standardul Unix recomandă ca rootfs-ul să aibă dimensiuni relativ mici, deoarece orice problemă sau corupere a acestuia poate împiedica inițializarea corectă a sistemului. Dacă, totuși, rootfs-ul devine corupt, există modalități prin care poate fi remediat. O soluție des folosită este de a monta acest sistem de fișiere într-un alt sistem, funcțional, pentru a putea fi explorat și verificat.

În cazul sistemelor embedded, de multe ori spațiul de stocare pe care îl avem la dispoziție este deja limitat: < 32MB. De cele mai multe ori, o bună parte din acest spațiu este ocupat de imaginea kernel-ului. De aceea, în majoritatea cazurilor, rootfs-ul pentru aceste sisteme este fie compus doar din minimul de programe necesar pentru inițializare, fie este stocat pe un server accesibil prin rețea și montat de către kernel la inițializare.

Formatul imaginii RaspberryPi

Imaginea recomandată pentru RaspberryPi, Raspbian, conține două partiții: o partiție FAT folosită pentru boot și o partiție ext4 pentru rootfs. Fiecare dintre aceste partiții începe de la un anumit offset în cadrul imaginii. Atunci când dorim să creăm o imagine nouă pentru RaspberryPi, trebuie să ne asigurăm că respectăm aceste partiții și formatele lor. Pașii pe care trebuie să îi urmăm sunt:

  • Stabilirea dimensiunii imaginii și inițializarea acesteia cu zero-uri
  • Crearea tabelei de partiții și a celor două partiții necesare
  • Formatarea partițiilor cu formatul corespunzător
  • Pentru popularea rootfs-ului, putem fie să montam partiția și să copiem manual directoarele și fișierele, fie putem să copiem o partiție întreagă de pe o altă imagine

Pentru fiecare dintre acești pași există utilitare ce ne ajută să realizăm operațiile necesare.

Tools

dd

Click to display ⇲

Click to hide ⇱

Pentru copierea, și eventual convertirea, unui fișier, la nivel de byte, se poate folosit utilitarul dd. Folosind dd, putem de asemenea genera fișiere de anumite dimensiuni și le putem stabili conținutul. În cazul nostru, dd este util pentru a inițializa o imagine și pentru a copia în acea imagine conținutul care ne interesează. Dintre parametrii lui dd, cei mai des utilizați sunt:

  • if - fișierul de intrare; dacă nu se specifică acest parametru, se va citi de la standard input
  • of - fișierul de ieșire; ca și la if, dacă nu este specificat, se scrie la standard output
  • count - numărul de blocuri de input ce vor fi copiate
  • bs - numărul de bytes dintr-un bloc

Un exemplu de utilizare a lui dd pentru a inițializa cu zerouri un fișier de o dimensiune exactă:

$ dd if=/dev/zero of=<file> bs=1k count=4096

Observați valorile lui count si bs. Se vor copia în total 4096 de blocuri de câte 1KB fiecare, rezultând o dimensiune de 4096KB. Fișierul de intrare /dev/zero este un fișier special din care se pot citi oricâte caractere ASCII NUL (0x00).

fdisk

Click to display ⇲

Click to hide ⇱

Pentru manipularea tabelei de partiții (crearea sau ștergerea partițiilor) se poate folosi utilitarul fdisk. Doar tabele de partiții DOS și disklabel-uri SUN și GNU sunt recunoscute și pot fi manipulate cu fdisk. Utilitarul primește ca parametru device-ul a cărui tabelă de partiții dorim să o modificăm. Un exemplu de apel este:

$ fdisk <device>

Rezultatul acestei comenzi este un prompt nou în care putem folosi tasta m pentru a afișa un meniu cu opțiunile disponibile. Opțiuni utile pentru crearea și ștergerea de partiții sunt:

  • p - afișează tabela curentă
  • d - șterge o partiție
  • n - creează o partiție nouă
  • x - expert mode
  • w - salvează tabela și iese.

Trebuie reținut că în mod normal fdisk operează cu sectoare și nu cu octeți. Atunci când adăugam o partiție, dacă specificăm doar dimensiunea ei și omitem unitatea de măsură (K, M, G etc.), fdisk va considera numărul specificat în sectoare.

  • Dimensiunea standard a unui sector de disc este 512 bytes. Aceasta poate fi schimbată folosind opțiunea -b.
  • Există două tipuri de partiții: primary și extended. Un device nu poate avea mai mult de 4 partiții primare.

mkfs

Click to display ⇲

Click to hide ⇱

Crearea unei partiții nu este suficient pentru a putea stoca fișiere. Avem nevoie ca partiția să conțină și un sistem de fișiere. Utilitarul folosit pentru a crea (formata) sisteme de fișiere Linux este mkfs. Parametrii pe care îi primește mkfs sunt device-ul ce trebuie formatat și tipul sistemului de fișiere dorit, specificat cu parametrul -t.

Comanda

$ mkfs -t ext4 /dev/fd0

va crea pe device-ul fd0 un sistem de fișiere ext4. În Linux, utilitarul mkfs este împărțit în câte un executabil pentru fiecare tip de sistem de fișiere suportat (mkfs.ext2, mkfs.ext4, mkfs.fat, etc.). Pentru a le vedea pe cele disponibile, deschideți un terminal, scrieți mkfs și apasați tab.

losetup

Click to display ⇲

Click to hide ⇱

În sistemele Unix, fișierele de tip block device sunt folosite pentru a expune sistemului dispozitivele fizice de stocare a datelor. Aceste fișiere se găsesc în directorul /dev/ în orice sistem Unix (/dev/sda, /dev/sdb, etc.). Pentru a putea crea block device-uri din imagini de sistem, vom folosi utilitarul losetup. Acesta creează o asociere între un loop block device (/dev/loop1) și un fișier care conține o imagine de sistem (raspberrypi.img). Astfel vom putea lucra cu imaginea aleasa ca și cum aceasta ar exista pe un suport fizic.

Principalele moduri de utilizare a utilitarului losetup sunt următoarele:

  • Crearea unui loop device dintr-un fișier de tip imagine
    • $ losetup -Pf <image file> 
  • Ștergerea unui loop device
    • $ losetup -d <loop device> 
  • Listarea tuturor asocierilor între loop devices și fisere
    • $ losetup 

lsblk

Click to display ⇲

Click to hide ⇱

Utilitarul lsblk este folosit pentru a afișa toate fișierele de tip block device din sistem, unde sunt acestea montate, precum și alte informații precum dimensiunea și tipul acestora.

Utilitarul lsblk apelat fară niciun parametru ne va oferi detalii despre toate device-urile, iar apelat cu un parametru reprezentând calea către device, ne vă afișa detalii doar despre acesta:

$ lsblk /dev/sda 
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0 51,3G  0 disk
├─sda1   8:1    0  512M  0 part /boot/efi
├─sda2   8:2    0    1K  0 part
└─sda5   8:5    0 50,8G  0 part /

mount

Click to display ⇲

Click to hide ⇱

În sistemele Unix, sistemul de fișiere este unul arborescent unde directoarele pot fi considerate noduri, iar fișierele frunze. Utilitarul mount ne permite să atașăm unui arbore existent un alt subarbore (sistem de fișiere).

Formatul general al comenzii este mount -t <type> <device> <mount path>, unde <type> este același ca la mkfs.

Network & Services

De multe ori, avem nevoie ca sistemele embedded sa porneasca pentru diverse servicii. Aceste servere, de obicei, expun un API de care ne putem folosi pentru a trimite comenzi si date sistemului. In acest laborator, vom invata cum sa implementam un server minimal si cum sa sa il facem sa porneasca la boot-time.

REST

REST (REpresentational State Transfer) este un set de arhitecturi software folosit pentru a ușura comunicarea intre clientul web și server-ul web. Implementarea server-ului și a clientului pot fi decuplate una de cealaltă, iar clientul poate face cereri (requests) către server pentru a primi sau modifica structurile de date de pe server, iar server-ul va raspunde acestor cereri (responses).

REST se bazează pe cele 4 tipuri de comenzi HTTP:

  • GET - primește o anumita resursă de pe server
  • POST - actualizează o resursă deja existantă
  • PUT - creează o nouă resursă
  • DELETE - șterge o resursă deja existantă

Cele 4 comenzi au rol similar celor din baze de date: CREATE, READ, UPDATE și DELETE

REST API

Similar cum API-ul (Application Programming Interface) din cadrul unui program C/Java/Python asigură inter-operatibilitatea componentelor acelui program, la fel si comunicarea REST dintre client și server se bazeaza pe un API.

Dacă în C/Java/Python apelăm funcții și pasăm argumente acelor funcții, în cazul REST-ului, în momentul în care clientul face un request către server, el are posibilitatea de a trimite funcția (adresa paginii) și argumentele funcției in următoarele trei feluri:

  • într-un query string - ex. /api/resource?p1=v1&p2=v2
  • parte din URL - ex. /api/resource/v1/v2
  • în interiorul cererii de HTTP - ex. prin pasarea de structuri JSON, MIME, XML

În cadrul exercițiilor de astăzi vom folosi a treia modalitate de pasare a argumentelor.

JSON

JSON (JavaScript Object Notation) este un format de date similar cu XML-ul, folosit pentru a trimite date independent de limbajul de programare.

Un exemplu de structura JSON este acesta:

{
  "firstName": "John",
  "lastName": "Smith",
  "isAlive": true,
  "age": 27,
  "address": {
    "streetAddress": "21 2nd Street",
    "city": "New York",
    "state": "NY",
    "postalCode": "10021-3100"
  },
  "phoneNumbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "office",
      "number": "646 555-4567"
    }
  ],
  "children": [],
  "spouse": null
}

Flask

În Python avem la dispoziție mai multe framework-uri web, printre care cel mai cunoscut este Django. Spre deosebire de Django, care deține mai multe tool-uri de folos pentru programator, Flask este un framework simplu ce permite usor crearea de prototipuri pentru proiecte mici.

Un program minimalist in Flask poate fi acesta:

hello_flask.py
import flask
 
app = flask.Flask(__name__)
 
@app.route('/', methods=['GET'])
def home():
    return "<h1>Hello World</h1>"
 
app.run()

În momentul rulării programului de Python, framework-ul va deschide un server web pe portul TCP 5000 (valoarea default), și va aștepta cereri HTTP de la un browser. În funcție de pagina cerută, Flask-ul va ruta cererea catre funcția de Python asociata (procedeu cunoscut drept mapping). În cazul acestui exemplu, când browser-ul va cere pagina principala, serverul web va returna un simplu header (H1) in format HTML.

Systemd services

Serviciile sunt procese care sunt executate automat de catre systemd in baza unor reguli definite pentru acel serviciu. Aceste reguli se regasesc in cadrul unui unit configuration file si ofera informatii despre fisierul care trebuie executat, precum si cand trebuie executat, dependente al serviciului, etc.

demo.service
[Unit]
Description=Demo service
After=network.target
StartLimitIntervalSec=0
 
[Service]
Type=simple
Restart=always
RestartSec=1
ExecStart=/usr/bin/myserver.sh
 
[Install]
WantedBy=multi-user.target
  • Description: Scurta descriere a seriviciului
  • After: Dependentile serviciului
  • ExecStart: Comanda care trebuie executata in cadrul serivicului

Fisierul care configureaza serviciul trebuie sa se regaseasca in cadrul directorului /etc/systemd/system/.

Pentru a porni serviciul la start-up, acesta trebuie sa fie enabled:

#: systemctl status demo.service 
○ demo.service - Demo service
     Loaded: loaded (/etc/systemd/system/demo.service; disabled>
     Active: inactive (dead)
     
#: systemctl enable demo.service
○ demo.service - Demo service
     Loaded: loaded (/etc/systemd/system/demo.service; enabled>
     Active: inactive (dead)
     

Pentru a porni serivicul manual vom folosi systemctl start:

#: systemctl start demo.service
● demo.service - Demo service
     Loaded: loaded (/etc/systemd/system/demo.service; enabled>
     Active: active (running) since Sun 2022-11-06 23:00:40 UTC; 6s ago
   Main PID: 697 (python3)
      Tasks: 3 (limit: 978)
     Memory: 50.0M
        CPU: 5.833s

Exerciții

Înainte de a începe exercițiile, asigurați-vă că aveți cel puțin 5GB de storage disponibili în mașină virtuala.

1. Dorim să ne creăm propria imagine pentru RaspberryPi-urile noastre. Pentru aceasta, vom folosi imaginea Debian 13 (Bookworm) pentru Raspberry Pi 3B+ ca și referință pentru imaginea noastră.

  • Aflați dimensiunea în MB a imaginii oficiale (ls -l --block-size=M).
  • Creați un fișier de dimensiunea necesară pentru imaginea completă, folosind dd.
  • Aflați informațiile tabelei de partiții a imaginii oficiale folosind fdisk.
  • Creați tabela de partiții și partițiile necesare pentru imaginea noastră.
  • Partițiile imaginii nou create trebuie să aibă același UUID ca și imaginea veche pentru a o folosi cu succes. Pentru a modifica UUID-ul diskului folosiți comanda x în consola deschisa de fdisk pentru a intra în expert mode. Folosiți comanda m pentru a vedea toate opțiunile și modificați disk identifier încât să corespunda cu cel listat pentru imaginea oficială.

  • Puteți folosi informațiile din tabela de partiții pentru a găsi numărul de blocuri ai fiecărei partiții, precum și offsetul lor în cadrul diskului virtual.
  • Asigurați-vă că tipurile partițiilor sunt aceleași precum în imaginea oficială. Pentru a schimba tipul unei partiții folosiți comanda t în cadrul consolei deschise de fdisk, urmată de codul asociat tipului pe care îl dorim.

2. În acest moment, cele doua partiții nu au niciun sistem de fișiere creat. Creați un loop block device pentru fiecare dintre cele doua imagini. Creați-le sistemele de fișiere, având grija la tipul acestora (revedeți secțiunea Formatul imaginii RaspberryPI). Odată create sistemele de fișiere, montați fiecare partiție într-un director.

In procesul de boot al sistemului, partitiile sunt cautate de kernel dupa label-ul acestora. Pentru a ne asigura ca sistemul booteaza cu succes, va trebui sa denumim partitiile conform imaginii de Debian.

Optiunea -L a comenzii mkfs.ext4, respectiv optiunea -n a comenzii mkfs.vfat ne ofera posibilitatea de a atribui un label atunci cand cream sistemul de fisiere.

Paritia de boot trebuie sa aiba label-ul RASPIFIRM, iar partitia pentru rootfs trebuie sa aiba label-ul RASPIROOT.

3. Ne dorim să copiem conținutul fișierelor din imaginea oficiala în imaginea noastră. Copiați conținutul directoarelor imaginii oficiale în directoarele imaginii noastre. După ce procesul de copiere s-a terminat, demontați device-urile și ștergeți loop device-urile.

Conținutul partiției de boot trebuie copiat în partiția de boot corespondenta din imaginea noastră. În mod similar, copierea trebuie făcută intre partițiile de rootfs.

4. Acum că imaginea noastră este gata, vrem să vedem că funcționează. Bootați sistemul în qemu folosind rootfs-ul nou creat.

5. Porniți server-ul web scris cu framework-ul Flask de aici home-automation.tgz

Pentru a-l rula, aveți nevoie de următorii pași de executat in Qemu/RPi:

  • Instalarea pachetului Python-Flask:
sudo apt install python3-pip

pip3 install flask
pip3 install flask-wtf
  • Deschideți server-ul cu următoarea comandă:
python3 home-automation.py
  • Serverul ruleaza in QEMU pe portul 5000. Pentru a avea acces la acesta, va trebui sa setam o noua regula de port-forwarding in optiunile comenzii qemu-system-aarch64:
...
-netdev user,id=net0,hostfwd=tcp::5555-:22,hostfwd=tcp::5000-:5000 \
...

Daca primiti eroare cand instalati pachetele necesare (Temporary failure in name resolution) , editati fisierul /etc/resolv.conf astfel incat sa aveti un nameserver valid.

nameserver 8.8.8.8

6. Momentan server-ul nu poate servi decât cererile de GET, iar aceeași pagină va fi afișata mereu. Pentru a schimba starea/structurile interne ale server-ului, vom folosi o cerere de REST API contruită de browser-ul web, cu ajutorului unui script JavaScript.

Pentru ca browser-ul să ruleze acest script, el trebuie să fie returnat odată cu conținutul paginii principale (index.html). Copiați conținutul fișierului de aici rest-api-request.js.txt, in interiorului fisierului templates/index.html, în secțiunea <script>.

Reîncărcați pagina web și dați click pe butonul de pornit lumina. O cerere de REST API va fi trimisă către server, dar va fi nevoie de un refresh al paginii pentru a lua ultima stare a structurilor.

7. Creati un serviciu pentru server-ul de Flask, astfel incat acesta sa porneasca automat la start-up. Revedeti sectiunea despre configurarea serviciilor .

Resurse

si/lab/2022/laboratoare/05.txt · Last modified: 2023/09/26 14:15 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0