dijous, 18 de febrer del 2021

Automatització de tasques d'administració del SO amb Python: introducció

Les tasques repetitives en l'administració de sistemes són habituals, com ara controls de error del sistema de fitxers o còpies de seguretat; la automatització de les tasques d'administració de sistemes informàtics consisteix en usar programari per crear instruccions i processos que reemplacen o redueixen la tasca manual dels administradors amb els sistemes. Es pot aplicar automatització a qualsevol tasca de la infraestructura informàtica, des de el sistema operatiu, passant per la xarxa fins la implementació de dominis de xarxa local o sistemes al  núvol.

La automatització es sol realitzar usant llenguatges d'script com ara en el cas de Linux amb el llenguatge propi del shell, o en el cas de Windows, amb el llenguatge Powershell. El llenguatge del shell de Linux és potent, no obstant això, a mesura que les tasques es tornen més complexes, els scripts de l'intèrpret d'ordres poden ser més difícils de mantenir. En el cas de Powershell és un llenguatge modern amb centenars de instruccions i llibreries. Podem utilitzar Python en lloc del shell script per a l’automatització. Python proporciona la mateixa funcionalitat que els scripts de l'intèrpret d'ordres però d’una manera més estructurada i escalable; a més a més Python s'executa també en Windows.

Les operacions bàsiques que es solen fer des del shell són:

  • Lectura de fitxers de configuració
  • Matar /crear processos
  • Creació / supressió de fitxers i directoris
  • Iniciar /aturar aplicacions
  • Gestió de fitxers

Els scripts de shell sovint gestionen també recursos de xarxa,  és a dir accedeixen a recursos mitjançant curl i wget

Mòdul os ()

Proporciona accés a les funcions d'alt nivell del SO; per carregar-la:

jordi@jordi-sve1513c5e:~$ python3
Python 3.8.5 (default, Jul 28 2020, 12:59:40)  
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>>

Les funcions que proporciona el mòdul estan en la ajuda oficial. Veiem alguns exemples d'ús. 

1. Python permet executar immediatament una ordre qualsevol del shell mitjançant la funció os.system(). Exemple d'ús:

jordi@jordi-sve1513c5e:~$ python3
Python 3.8.5 (default, Jul 28 2020, 12:59:40)  
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.system("ls -l -d")
drwxr-xr-x 37 jordi fisics 4096 de febr. 17 09:01 .

2. Llistar directoris:

>>> llista = os.listdir()
>>> for directori in llista:
...  print(directori)
... 

3. Veure la ruta d'un fitxer

>>> os.path.abspath('prova.txt')
'/home/jordi/prova.txt'
>>>

4. Separar cada directori de la ruta actual en cadenes de text separades:

>>> os.getcwd()
'/home/jordi/Documents/Feina'
>>> os.path.split(os.getcwd())
('/home/jordi/Documents', 'Feina')
>>>

5. Comprovar si existeix una carpeta:

>>> os.path.exists('/home/jordi/DAM')        
False

6. Accedir a les variables d'entorn:

>>> os.environ['HOME']
'/home/jordi'
>>> os.environ['USER']
'jordi'
>>>


NOTA: Com ja s'ha dit una de les avantatges de Python és que funciona tant en Windows con en Linux (i també en altres SO), així, l'exemple anterior en Windows només canviaria en el nom de les variables d'entorn, la imatge següent ho mostra en un Windows 8:



7. Obrir i llegir fitxers de text

Les funcions os.accessopen i read ens permeten comprovar si un fitxer existeix (per exemple un fitxer de configuració), obrir-lo i mostrar-lo per pantalla, per exemple.

>>> if os.access("prova", os.R_OK):
...  with open("prova") as f:       
...   l = f.read()
...   print(l)
...  
hola
adeu

Fixem-nos amb l´ús de la funció os.acces, obre el fitxer prova amb al paràmetre mode d'accés os.R_OK que serveix per comprovar si el fitxer és llegible. Per exemple si tenim aquest dos fitxers:

jordi@jordi-sve1513c5e:~$ sudo ls -l prova*
-r-------- 1 root  fisics   10 de febr. 18 11:08 prova
-rw-r--r-- 1 jordi fisics   13 de des.  12 11:40 prova.txt

Llavors el primer fitxer només serà llegit si executem Python com a root. Ampliem el codi de l'exemple, usant aquest cop un editor de text, incloent una lectura del nom del fitxer (funció input) i un missatge si no es pot accedir:

import os
print("fitxer a llegir?")
fitxer = input()
if os.access(fitxer, os.R_OK):
   with open(fitxer) as f:
        l = f.read()
        print(l)
   print("fitxer comprovat i llegit") 
else:
   print("fitxer sense accés de lectura")


I l'executem amb els dos fitxers, sense accés de root:

jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$ python3 llegir_fitxer.py  
fitxer a llegir?
prova
fitxer sense accés de lectura
jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$ python3 llegir_fitxer.py  
fitxer a llegir?
prova.txt
hola que tal

fitxer comprovat i llegit
jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$


8. Escriure en un fitxer

Per escriure a un fitxer existent, heu d'afegir un paràmetre a la funció open ():

"a" - Afegeix - s'afegirà al final del fitxer

"w" - Escriu - sobreescriurà qualsevol contingut existent

Exemple: per afegir una ruta a la variable d'entorn $PATH podem fer:

jordi@jordi-sve1513c5e:~$ PATH="$HOME/Imatges:$PATH"    
jordi@jordi-sve1513c5e:~$ echo $PATH
/home/jordi/Imatges:/home/jordi/.local/bin:/home/jordi/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/
sbin:/bin:/usr/games:/usr/local/games:/snap/bin
jordi@jordi-sve1513c5e:~$

El contingut de $PATH s'inicialitza quan l'usuari obre sessió al llegir-se el fiter ocult .profile; escrivim un script que afegeixi una ruta qualsevol a la variable $PATH dintre del fitxer de perfil de l'usuari.

import os
print("ruta a afegir al PATH?, ha de ser una subcarpeta del directori HOME de l'usuari actiu")
ruta = input()
path = "$HOME/" + ruta + ":" + os.environ['PATH']
fitxer = ".profile"
if os.access(fitxer, os.W_OK):
   with open(fitxer,"a") as f:
        f.write("PATH=" + path +'\n')
        f.close()
   print("PATH actualitzat!") 
else:
   print("fitxer sense accés de lectura")

Fixeu-vos en l'ús de l'operador "+" per unir cadenes de text, i també en el caràcter especial "\n" que representa un salt de línia. El provem:

jordi@jordi-sve1513c5e:~$ python3 ./Documents/Feina/M6\ 2020/UF2/modificar_profile.py            
ruta a afegir al PATH?, ha de ser una subcarpeta del directori HOME de l'usuari actiu
Documents/Feina/M6 2020 
PATH actualitzat!

Per comprovar que realment està modificat mirem el contingut del .profile:

# set PATH so it includes user's private bin if it exists 
if [ -d "$HOME/.local/bin" ] ; then
   PATH="$HOME/.local/bin:$PATH"
fi
PATH=$HOME/Documents/Feina/M6 2020:/home/jordi/Imatges:/home/jordi/.local/bin:/home/jordi/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin


S'ha afegit correctament el nou camí a la variable PATH dins del fitxer de configuració del perfil.

Teniu tutorials de gestió bàsica de fitxers en la w3school

Automatització: introducció

Ara que tenim un mínim de nocions de Python aplicat a la gestió del sistema de fitxers, podem fer una primera incursió en la automatització de la administració del sistema operatiu. Un script senzill que pot ser de certa utilitat és l'script buscar_en_passwd_i_afegir.py, en la versió 1.0:

import os
fitxer = "/etc/passwd"
acabat = False                          # variable booleana
while not acabat:
 print ("usuari a buscar en el sistema?")
 user = input()
 if not user: break
 èxit = False                         # variable booleana
 if os.access(fitxer, os.R_OK):
    with open(fitxer) as f:
        tot = f.read()                # llegeix tot el fitxer de cop
        trobat =  (user in tot)         # cerca en tota el string, hi és?
        if trobat:
            print("usuari trobat")     # no cal afegir l'usuari, ja existeix
            èxit = True  
    f.close()
 if not èxit:                            # no existeix, afegir-lo
   print("no hi és, afegint-lo...")
   os.system("sudo useradd " + user)    # passem ordres al shell
   os.system("sudo passwd -e " + user)
   print ("Fet!")
   
L'script, el que fa és anar preguntant repetidament un nom d'usuari que buscarà en  /etc/passwd, si el troba ho indica, i si no el troba crea un usuari amb el nom donat, deixant la contrasenya nul·la de forma que quan el nou usuari entri per primer cop li demanarà que l'escrigui, això ho aconseguim usant os.system que recordem crida comandaments del shell directament. La repetició es trenca quan introduïm un nom sense contingut. Si el provem:

 jordi@jordi-sve1513c5e:~$ python3 ./Documents/Feina/M6\ 2020/UF2/buscar_en_passwd_i_afegir.py  
usuari a buscar en el sistema?
jordic 
usuari trobat
usuari a buscar en el sistema?
jordi
usuari trobat
usuari a buscar en el sistema?
PereElMagnífic 
no hi és, afegint-lo...
passwd: password expiry information changed.
Fet!
usuari a buscar en el sistema?

jordi@jordi-sve1513c5e:~$

Si no estem com a root, la instrucció sudo farà que l'escript s'aturi per tal de que el shell ens pregunti la contrasenya de root. 

EXERCICIS PROPOSATS

Exercici 1. Modifiqueu l'exemple de l'apartat 8 per tal de que afegeixi la instrucció umask 011 al final del fitxer .profile. Proveu-lo.

Exercici 2. Escriu un script buscar_en_profile.py Python que demani una paraula i la busqui en cada línia del fitxer .profile; per cada línia que trobi, escriu un missatge i mostra la línia. En acabar, si no ha trobat cap línia, mostra el missatge "no hi és". Exemple d'execució:

jordi@jordi-sve1513c5e:~$ python3 ./Documents/Feina/M6\ 2020/UF2/buscar_en_profile.py  
paraula a buscar en .profile?
umask
trobada a la línia: '# the default umask is set in /etc/profile; for setting the umask
'
trobada a la línia: '# for ssh logins, install and configure the libpam-umask package.
'
trobada a la línia: '#umask 022
'
trobada a la línia: 'umask 011'


Ajudes: per llegir línia a línia useu la funció readline() en comptes de la read (aquesta llegeix tot el fitxer de cop), per buscar una paraula en una línia de text useu la sentència in (veure exemple en el tutorial de Python en w3schools), serà útil que us acostumeu a usar variables i constants lògiques True / False. Us caldrà també una estructura repetitiva per recórrer totes les línies del fitxer, com a suggerència, podeu usar aquesta:

while (True):
        línia = fitxer.readline()
            ( . . .resta de línies del programa on fem coses amb la línia)
        if not línea:
            break

La instrucció break força a sortir de la repetició iniciada amb while, i com hem usat com a condició del while la constant True, equival a dir: comença sempre a llegir, si veus que no hi ha res per llegir (sentència if not línea ) llavors trenca (break) la repetició. 
  

Exercici 3. Com es podria modificar l'script buscar_en_passwd_i_afegir.py, per tal de que en comptes de preguntar cada usuari a afegir, llegeixi aquests usuaris d'un fitxer de text que li passem com a paràmetre?. L'script ha de comprovar que tal fitxer és accessible per lectura.  


dimecres, 10 de febrer del 2021

Introducció a Python: variables simples, llistes

Python és un llenguatge modern i potent, amb poc esforç es poden programar accions que en altres llenguatges de programació resulten laborioses. És del tipus interpretat, com els llenguatges d'script, i orientat a objectes. En aquest article usem dos recursos: 1) el intèrpret en línia de Python en la versió 3, en un Linux Ubuntu 20.04, els exemples s'identifiquen per que les línies comencen ammb els símbols >> > com ara:

jordi@jordi-sve1513c5e:~$ python3
Python 3.8.5 (default, Jul 28 2020, 12:59:40)  
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

També usarem els tutorials de https://www.w3schools.com/python que permeten  executar codi, seran els exemples sense els ">>>".

Variables simples

Les variables es declaren simplement usant-les:

>>> salutacio="hola"
>>> salutacio
'hola'
>>> salari=40000
>>> salari
40000

Hem creat, i mostrat a continuació, una variable de tipus text i un altre de tipus numèric enter. De la mateixa forma directa podem crear variables de tipus numèric amb decimals i usar Python com a calculadora:

>>> irpf=22.4
>>> irpf
22.4
>>> retencio=6000*irpf/100
>>> retencio
1344.0
>>>

Les variables són molt flexibles, poden anar canviar de contingut sense cap problema:

from datetime import datetime
time = datetime.now()     #importa data i hora sistema
x = 5                   #variable x numèrica
y = "John Dillinger"
x = (time.strftime("%I:%M:%S")) #variable x data
print(x)
print(y)

 


En cada instant podem comprovar el tipus de variable que és:

>>> type(x)
<class 'int'>
>>>

Cal tenir en compte que es diferencia entre majúscules i minúscules:

>>> X="Python"
>>> print(x)
100
>>> print(X)
Python
>>>

Es poden donar valors de forma compacta:

>>> x , X = 100, "Python"
>>> print(x)
100
>>> print(X)
Python
>>>
 

Els diferents tipus de variables permesos són:

Text:str
Numeric: int, float, complex
Llistes: list, tuple, range
Mapeig: dict
Conjunts: set, frozenset
Booleà: bool
Binari:bytes, bytearray, memoryview

 Variables de tipus objecte i mòduls Python

Les variables de tipus objecte a més a més d'un valor també tenen associades accions, es poden fer accions predeterminades amb elles. Un exemple seria una variable de tipus data per contindre un dia del calendari;  Python disposa de mòduls, que venen a ser una mena de plug-ins que es poden cridar amb la instrucció import per usar-los si ens convé, entre aquests mòduls està time.

>>> import time
>>> time.strftime("%d/%m/%y")
'10/02/21'
>>>

Si mirem el tipus de variable que és time veiem que ens diu "classe mòdul":

>>> import time
>>> type(time)
<class 'module'>
>>>
 

La funció strftime  forma part de la classe time extreu la data del sistema, en el format especificat. En general és recomanable usar print per mostrar variables:

>>> print (time.strftime("%I:%M:%S"))
01:49:34
>>>

Les variables de tipus objecte s'agrupen en classes d'objectes; per exemple podem crear una classe de forma simple:

>>> class MyClass:
...   x = 5
...  
>>> p1 = MyClass()
>>> print(p1.x)
5
>>>

defineix una classe d'objectes MyClass; qualsevol variable que formi part de la classe contindrà una subvariable x (més correctament, una propietat de la classe). Les dues primeres línes defineixen la classe, la tercera (en blanc) acaba la definició, després creem la variable p1 del tipus classe Myclass, i per últim mostrem la seva propietat x

En Python la indentació és necessària, indica quan acaben les instruccions que ocupen diverses línies. Per exemple:

 >>> class Myclass:
... x = 5
 File "<stdin>", line 2
   x = 5
   ^
IndentationError: expected an indented block

obtenim un error degut a que no hem indentat la línia de declaració de la propietat x; cal fer-ho amb el tabulador.

En aquesta introducció no entrarem en detalls de la programació orientada a objectes (constructors, destructors, etc).

Llistes

Les llistes són variables que poden guardar molt valors de forma ordenada, estructurada, classificant cada valor segons la seva posició a la llista, començant per la posició 0:

>>> llista = ["Python","Powershell", "C", "Java"]
>>> print (llista[0])
Python
>>> print (llista)
['Python', 'Powershell', 'C', 'Java']
>>>

Els valors poden ser de qualsevol tipus:

>>> llista = ["Python",100, 3.14159]
 

En realitat les llistes són objectes de la classe list:

>>> type(llista)
<class 'list'>

Per tant qualsevol llista contindrà certes propietats i accions predefinides; per exemple per ordenar alfabèticament la llista:


llista = ["Python","Powershell", "C", "Java"]
llista.sort()
print(llista)

['C', 'Java', 'Powershell', 'Python']

 
Les llistes contenen molt altres mètodes, veure.

Podem modificar qualsevol valor donant la seva posició:

 >>> llista[1]="Powershell"

Es pot canviar un valor per varis valors, insertant-los usant un rang com ara:

>>> llista
['Python', 'Powershell', 3.14159]
>>> llista[1:2]=["C", "Yaml"]
>>> llista
['Python', 'C', 'Yaml', 3.14159]
>>>
 
 

veiem que el valor 1, "Powershell" ha estat subsistit per 2 valors "C" i "Yaml".

Llistes de llistes

Com dintre d'una llista podem ficar qualsevol variable de qualsevol tipus, fins i tot podem ficar llistes dintre de llistes: 

>>> Llenguatges=[llista, "javascript"]
>>> print(Llenguatges)
[['C', 'Java', 'Powershell', 'Python'], 'javascript']
>>>


Observem que la primera dada de la llista Llenguatges és un altre llista. En el cas particular de que una llista contingui exclusivament altres llistes, tindrem una "llista de llistes", que sovint en l'argot de programació s'anomena una matriu:

>>> Llenguatges=[llista, ["javascript","Java","PHP","CSS"]]
>>> print(Llenguatges)
[['C', 'Java', 'Powershell', 'Python'], ['javascript', 'Java', 'PHP', 'CSS']]
>>>


Python permet crear llistes inicialment buides, i després anar afegint elements conforme necessitem espai usant la funció predeterminada append:

llista = []
llista.append("a")
llista.append("b")
print(llista)

 ['a', 'b']

També amb append podem afegir llistes a llistes:

>>> comarques_famílies=[]
>>> comarques_famílies.append(['Baix Camp'])
>>> comarques_famílies.append(['Alt Camp'])    
>>> print (comarques_famílies)                 
[['Baix Camp'], ['Alt Camp']]

Fixem-nos amb els dobles claudàtors, indiquen que tenim una llista de llistes amb dues llistes, cada una amb un únic element. Anem a afegir un segon element en la primera llista:

comarques_famílies[0].append('INS Baix Camp')

>>> print (comarques_famílies)                 
[['Baix Camp', 'INS Baix Camp'], ['Alt Camp']]
>>>
 

El contrari d'afegir és eliminar: remove elimina el 1r element que coincideixi amb l'especificat; fixem-nos en què passa amb aquesta inserció:

 >>> comarques_famílies.append('INS Baix Camp')

>>> print(comarques_famílies)
[['Baix Camp', 'INS Baix Camp'], ['Alt Camp'], 'INS Baix Camp']
>>>
 

Com no hem especificat en quina llista volem inserir el valor, s'ha inserit al final, fora de les dues llistes; suposem que no és el que voliem fer, i ara volem esborrar aquest valor; 

>>> comarques_famílies.remove('INS Baix Camp')
>>> print(comarques_famílies)                  
[['Baix Camp', 'INS Baix Camp'], ['Alt Camp']]

Fet. No ha esborrat el valor que està dins de la 1a subllista per que no ho hem especificat; en canvi si fem  

>>> comarques_famílies[0].remove('INS Baix Camp')
>>> print(comarques_famílies)                     
[['Baix Camp'], ['Alt Camp']]
 

ara sí ha eliminat el valor de la 1a llista.

Una altre característica molt flexible és l'anomenar cada llista amb un nom, i juntar-les totes en una llista de llistes amb noms:

>>> a = ["apple", "banana", "cherry"]
>>> b = ["Ford", "BMW", "Volvo"]
>>> c = [a, b]
>>> print(c)
[['apple', 'banana', 'cherry'], ['Ford', 'BMW', 'Volvo']]
>>>

Si després modifiquen les llistes a o b, la llista de llistes c automàticament s'actualitza:

>>> a.append("orange")
>>> b.append("Honda")
>>> print(c)           
[['apple', 'banana', 'cherry', 'orange'], ['Ford', 'BMW', 'Volvo', 'Honda']]
>>>

Fitxers font Python

Si guardem el codi font de Python en un fitxer de text, per exemple fitxer.py, després podrem executar-lo des del shell simplement escrivint:

python3 fitxer.py

La majoria d'editors de codi reconeixen l'extensió .py i per tant reconeixen les instruccions Python. 

Lectura de dades externes al programa

Si importem el mòdul sys obtenim l'objecte stdin (entrada estàndard), que conté la funció readlines() capaç de llegir un fitxer des de l'entrada estàndard línia a línia. Per exemple, el següent programa:

import sys
for order in sys.stdin.readlines():

    print(order)

llegirà de l'entrada estàndard (el teclat) línia a línia de text i la mostrarà per la sortida estàndard (el monitor):

jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$ python3 linia.py
hola
adeu

El problema és que la lectura no té final; però si redireccionem l'entrada estàndard a un fitxer de text (l'anomenem prova en l'exemple a continuació), llavors sí s'atura en arribar al final del fitxer:

jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$ cat prova | python3 linia.py
linia 1

linia 2

jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$

Les línies en blanc es poden eliminar usant la funció strip de la classe string (cadenes de text) doncs la variable order que retorna la lectura és un string:

import sys
for order in sys.stdin.readlines():
    linia = order.strip()
    print(linia)

Hem usat l'anomada estructura de control de programa for ... in, que té la segënt estructura general

for variable in llista:

    accions

La classe stdout del mòdul sys té la funció write que permet escriure resultats amb format més complert que la funció simple print:

import sys
for order in sys.stdin.readlines():
    linia = order.strip()
    print(linia)
sys.stdout.write("darrera línia llegida: %s\n" % linia)

Resultat:

jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$ cat prova | python3 linia.py
linia 1
linia 2
darrera línia llegida: linia 2
jordi@jordi-sve1513c5e:~/Documents/Feina/M6 2020/UF2$

Exercicis

1. Tenim un fitxer de text de 4 línies amb el següent contingut:

Baix Camp;43002594 Institut Baix Camp;Serveis Socioculturals i a la Comunitat;Instal·lació i Manteniment;Serveis Socioculturals i a la Comunitat;Vidre i Ceràmica;Indústries Alimentàries
Baix Camp;17001759 Ins Escola d'Hoteleria i Turisme;Hoteleria i Turisme
Tarragona;43006630 Institut Pere Martell (Tarragona);Administració i Gestió;Fusta, Moble i Suro;Informàtica i Comunicacions;Tèxtil, Confecció i Pell;Indústries Alimentàries
Tarragona;43003653 Institut Comte de Rius (Tarragona);Transport i Manteniment de Vehicles;Instal·lació i Manteniment;Comerç i Màrqueting;Serveis Socioculturals i a la Comunitat

Llegiu totes les línies del fitxer i guardeu cada línia en una llista de Python, cada element de la llista serà un valor de tipus String.

 2. Seguint amb l'exercici anterior; la funció count de la classe string retorna el nombre de ocurrències d'un text dins d'un string. Sabent que el fitxer de centres d'FP conté en cada fila la comarca, el nom d'un centre d'FP, i una llista d'especialitats, tot separat per comes, useu count per comptar els ";" de cada fila, li resteu 1, i mostreu el text "nombre d'especialitats:" i seguidament el valor calculat.

3. Seguint amb l'exercici anterior, volem formar una matriu que contingui per cada línia la comarca i el nombre d'especialitats de cada comarca; 

 ['Priorat', 2, 'Alt Camp', 4, 'Tarragona', 5, 'Tarragona', 3, 'Baix Camp', 5, 'Baix Camp', 1, 'Tarragona', 5, 'Tarragona', 4]


Per extreure el nom de la comarca podeu usar alguna de les funcions de la classe string; suggeriment: la funció split "trenca" un string en elements individuals i els deixa en una llista

linies = linia.split(";")

deixarà en la llista línies cada dada (nom de comarca, nom de centre, ...) com a element de la llista línies.











dimarts, 9 de febrer del 2021

Exemple de creació d’un clúster Kubernetes en el núvol

Què és Kubernetes?

 Es sol dir que és un «orquestrador» de contenidors d’aplicacions, més tècnicament, un middleware que permet integrar serveis provinents de diverses fonts, i proveir informació de forma síncrona o asíncrona. Està escrit en Go, creat per Google i cedit com programari lliure a la Cloud Native Computing Foundation (CNCF, https://www.cncf.io/ ).

S’estructura en Pods, la unitat mínima de computació, que permet executar un conjunt de contenidors sota una única IP, i el ReplicaSets, que controla les rèpliques de cada Pod. Aporta tolerància a errades, alta disponibilitat i escalabilitat en calent. A més a més permet fer instal·lacions i actualitzacions automàtiques, i mitjançant el proxy invers Ingress permet fer balanç de càrrega entre servidors (nodes). Els pods s'executen en nodes, que són màquines virtuals funcionant de forma sincronitzada formant un clúster.

Crear des de zero, usant màquines virtuals, un clúster Kubernetes no és tasca fàcil, i a més a més per fer-ho bé calen un mínim de tres màquines.  El més habitual en la pràctica és contractar un servei de virtualització en el núvol que facilita molt la tasca. En aquesta entrada de bloc veurem un exemple de creació de clúster en el núvol, en el que iniciarem un servei web simple, també habilitarem una interfície web de control del clúster.

Pas 1: creació del «pool» de nodes

Usarem el proveïdor OVH → Public Cloud Services → Crear un cluster Kubernetes; inicia un assistent:

 

Tres nodes és el mínim «sensat»: un node actua de màster i els altres dos s’anomenen «minions»: El màster no executa contenidors, són els encarregats de decidir en quin node (minion) s'executa cada contenidor, de mantenir l'estat eficient del clúster, d'assegurar que en tot moment hi ha el nombre desitjat de contenidors en execució i d'actualitzar les aplicacions de forma coordinada quan es despleguen noves versions. Node minion és aquell que executa els contenidors desplegats al clúster. De sèrie, Kubernetes pot executar contenidors Docker i RKT, sent possible integrar-lo amb altres motors existents a través de CRI (Container Runtime Interface).

 

Cada node és una màquina virtuals, però amb aquesta opció activada cada MV estarà en una màquina física diferent, per millorar la disponibilitat, si estan totes les MV en una màquina física si cau aquesta cau tot el clúster!

 

Ja el tenim creat.

Ens proporcionen les dades d’accés al clúster al núvol:

 


Ara mateix no hi han encara contenidors en execució:

 


 

Preparar accés al clúster des de màquina local

Per administrar el clúster remotament ens cal instal·lar el comandament kubectl en la nostra màquina local (la de casa, per entendre'ns). Primer el descarreguem:

 

jordi@jordi-sve1513c5e:~$ sudo curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/rel
ease/stable.txt)/bin/linux/amd64/kubectl"
[sudo] password for jordi:  
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100   161  100   161    0     0    851      0 --:--:-- --:--:-- --:--:--   851
100 38.3M  100 38.3M    0     0  5580k      0  0:00:07  0:00:07 --:--:-- 5970k
jordi@jordi-sve1513c5e:~$

 

Validar la descàrrega amb el hash:


jordi@jordi-sve1513c5e:~$ curl -LO "https://dl.k8s.io/$(curl -L -s https://dl.k8s.io/release/stable.t
xt)/bin/linux/amd64/kubectl.sha256"
 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100   161  100   161    0     0    914      0 --:--:-- --:--:-- --:--:--   914
100    64  100    64    0     0    117      0 --:--:-- --:--:-- --:--:--   117
jordi@jordi-sve1513c5e:~$ echo "$(<kubectl.sha256) kubectl" | sha256sum --check
kubectl:
OK
jordi@jordi-sve1513c5e:~$


Instal·lar el comandament:


jordi@jordi-sve1513c5e:~$ sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
jordi@jordi-sve1513c5e:~$


NOTA: en entorn real caldrà sempre establir un tunel VPN i usar el comandament kubectl dins de la VPN! És altament insegur fer administració remota usant una connexió pública! Com hi ha una certa confusió entre connexió encriptada (com ara ssh) i xarxa privada virtual (VPN), convé un breu repàs.

  • Les connexions tipus SSH (com ara la de kubectl) connecten una màquina amb una altre màquina, a través de una xarxa local o d'una xarxa pública, encriptant la transmissió. 
  • Les connexions VPN en canvi connecten una màquina a una xarxa local sencera, també encriptant, de forma que la màquina es com si estigues dintre de la xarxa local remota, per això se li diu "xarxa privada (o local, no pública) virtual": l'equip remot es converteix en part de la xarxa a la que es connecta, i té accés complert a tota la xarxa. El tràfic de informació provinent dels  usuaris de la xarxa remota es vist com provinent d'una única adreça IP pública; això dificulta el seguiment d'un usuari de la xarxa remota doncs queda ocult en una única IP.
  • Per últim tenim les connexions ssh amb túnel, que són diferents a la VPN: ssh per defecte obre una sessió de shell (és a dir, el vostre terminal local està connectat a un shell que s’executa al sistema remot), però hi ha diferents tipus de dades que es poden transportar, un d’aquests tipus és una connexió de socket, per exemple, una connexió TCP: això s'anomena un túnel SSH; el client SSH escolta en un port TCP i transporta totes les connexions realitzades a aquest port a través del canal segur i surt de la connexió des del sistema remot. Per defecte, quan configureu un túnel, encara obtindreu un intèrpret d’ordres, SSH transporta dos tipus de dades per la mateixa connexió: el túnel i l’intèrpret d’ordres. A més a més les connexions SSH via túnel poden dirigir-se a través de ports TCP arbitraris

 

Configuració de la connexió remota amb el cluster

Descarreguem el fitxer de configuració del clúster proporcionat pel proveïdor del servei fins la màquina local, i el guardem en el directori ocult $HOME/.kube amb el nom config

 


El fitxer descarregat és quelcom semblant a: 

apiVersion: v1

clusters:

- cluster:

certificate-authority-data: LS0tLS1CRUdJTiBD(...)==

server: https://9tvsxd.c1.gra7.k8s.ovh.net

name: ASIXcs

contexts:

- context:

cluster: ASIXcs

user: kubernetes-admin-ASIXcs

name: kubernetes-admin@ASIXcs

current-context: kubernetes-admin@ASIXcs

kind: Config

preferences: {}

users:

- name: kubernetes-admin-ASIXcs

user:

client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ (etc ...)



Creem a continuació el servei "demo" proporcionat per el proveïdor hello-world, activant el balanç de càrrega entre nodes; el servei es  defineix pel fitxer de configuració en llenguatge yaml següent:


apiVersion: v1

kind: Service

metadata:

name: hello-world

labels:

app: hello-world

spec:

type: LoadBalancer

ports:

- port: 80

targetPort: 80

protocol: TCP

name: http

selector:

app: hello-world

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: hello-world-deployment

labels:

app: hello-world

spec:

replicas: 1

selector:

matchLabels:

app: hello-world

template:

metadata:

labels:

app: hello-world

spec:

containers:

- name: hello-world

image: ovhplatform/hello

ports:

- containerPort: 80



Iniciem el servei amb el comandament kubectl:


jordi@jordi-sve1513c5e:~$ kubectl apply -f hello.yml
service/hello-world created
deployment.apps/hello-world-deployment created
jordi@jordi-sve1513c5e:~$

Llistem els contenidors (pods) i serveis (deploys) disponibles per veure que està actiu:

jordi@jordi-sve1513c5e:~$ kubectl -n=default get pods
NAME                                      READY   STATUS    RESTARTS   AGE
hello-world-deployment-559d658ffb-pdh8l   1/1     Running   0          2m56s
jordi@jordi-sve1513c5e:~$

jordi@jordi-sve1513c5e:~$ kubectl -n=default get deploy
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
hello-world-deployment   1/1     1            1           3m42s
jordi@jordi-sve1513c5e:~$

Llistem també els serveis disponibles, el que hem iniciat dona servei TCP/IP pel port 80 i és de l tipus servidor web:

jordi@jordi-sve1513c5e:~$ kubectl -n=default get services 
NAME          TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
hello-world   LoadBalancer   10.3.125.127   51.210.210.34   80:32452/TCP   4m37s

kubernetes    ClusterIP      10.3.0.1       <none>          443/TCP        39m
jordi@jordi-sve1513c5e:~$
Aquest servei de demostració és un servidor web que només retorna una pàgina; ara que està actiu, amb el navegador, anem a la IP externa per provar el servei: 

 

Aquest servei web s'està executant simultàniament en dos nodes (màquines virtuals) situades en diferents màquines físiques, dintre de contenidors Docker; un tercer node, el màster, s'encarrega de l'orquestració (balanç de càrrega i alta disponibilitat).

 

Instal·lació del tauler de control de Kubernetes


És una interfície web d’aministració per Kubernetes. La baixem i instal·lem mitjançant el comandament kubectl i un fitxer de configuració:


jordi@jordi-sve1513c5e:~$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.
0.0-rc7/aio/deploy/recommended.yaml


namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created
jordi@jordi-sve1513c5e:~$

Ara creem un compte d’administració remota, usant un fitxer yaml


apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kubernetes-dashboard

i el comandament kubectl:



jordi@jordi-sve1513c5e:~$ kubectl apply -f dashboard-service-account.yml                             
serviceaccount/admin-user created
jordi@jordi-sve1513c5e:~$

a continuació, apliqueu el fitxer per afegir el compte de servei al clúster:


jordi@jordi-sve1513c5e:~$ kubectl apply -f dashboard-cluster-role-binding.yml
Warning: rbac.authorization.k8s.io/v1beta1 ClusterRoleBinding is deprecated in v1.17+, unavailable in
v1.22+; use rbac.authorization.k8s.io/v1 ClusterRoleBinding
clusterrolebinding.rbac.authorization.k8s.io/admin-user created
jordi@jordi-sve1513c5e:~$

després, utilitzant la funció d'administrador de clústers per al vostre clúster, crearem un RoleBinding, que el vincularà al vostre compte de serveis, amb el següent yaml:


apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kubernetes-dashboard

executem:


jordi@jordi-sve1513c5e:~$ kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-das
hboard get secret | grep admin-user-token | awk '{print $1}')
Name:         admin-user-token-ggd8b
Namespace:    kubernetes-dashboard
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: admin-user
             kubernetes.io/service-account.uid: e2d4cfa2-1bd3-4c09-a2bb-e671736642f3

Type:  kubernetes.io/service-account-token

Data
====
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6Ii1pMEpPRkdaRGNZaC1UajZXMi05S0hkRzdoMVN2QWlUbGZmS3BDUlpBeWMif
.......
bMQTWwtzkLz4O_Rb7_FETXBJKtgMkTJBnRzeyYZ7VUnNixpsAjLftn-6KQ-KfFFTM_c4xFvodjW8ZQCMIRzbVS8
ca.crt:     1801 bytes
namespace:  20 bytes
jordi@jordi-sve1513c5e:~$


Iniciem l’aplicació:


jordi@jordi-sve1513c5e:~$ kubectl proxy
Starting to serve on 127.0.0.1:8001

Accedim a la interfície web d’administració, ens demana la cadena «token» encriptada com a contrasenya, i entrem a la pantalla principal:


La interfície web  pot desplegar aplicacions en contenidors d'un clúster de Kubernetes, solucionar problemes de l'aplicació, gestionar els recursos del clúster, donar una visió general de les aplicacions que s’executen al clúster, crear o modificar recursos de Kubernetes individuals (com ara deploys).  Per exemple, podeu escalar un deploy (escalar vol dir donar o treure-li recursos) iniciar una actualització, reiniciar un pod o desplegar noves aplicacions mitjançant un assistent de desplegament.

Tutorial interactiu de Kubernetes: https://kubernetes.io/docs/tutorials/kubernetes-basics/







 

 

 

 

 

 




 






Gestió d'usuaris i grups en Linux

Usuaris i grups Linux  Els comptes de Linux són com els comptes de Windows o MacOS; però els detalls no, així que cal explicar alguns detall...