A Puppet bevezetése

A Puppet egy konfigurációkezelő rendszer. Használják a hosztok kívánt állapotának elérésére és fenntartására.

Több mint öt éve dolgozom a Puppet-tel. Ez a szöveg lényegében a hivatalos dokumentáció kulcspontjainak fordított és átrendezett gyűjteménye, amely lehetővé teszi a kezdők számára, hogy gyorsan megértsék a Puppet lényegét.

A Puppet bevezetése

Alapinformációk

A Puppet működési sémája kliens-szerver alapú, bár létezik korlátozott funkcionalitással bíró nélküli szerver opció is.

Pull modellt használ: alapértelmezés szerint a kliensek félóránként hívják a szervert a konfigurációért és alkalmazzák azt. Ha dolgozott már az Ansible-lal, ott push modellt használnak: az adminisztrátor kezdeményezi a konfiguráció alkalmazásának folyamatát, a kliensek maguktól nem fognak semmit alkalmazni.

Hálózati interakció során kölcsönös TLS titkosítást alkalmaznak: a szervernek és a kliensnek van saját titkos kulcsa és a megfelelő tanúsítványai. Általában a szerver adja ki a tanúsítványokat a klienseknek, de elvileg lehetséges külső CA használata is.

Megismerkedés a mítoszokkal

A Puppet terminológiájában a papet szerverhez csatlakoznak nódok (nodes). A nódok konfigurációját mint a mítoszokban egy speciális programozási nyelven, a Puppet DSL-en írják.

A Puppet DSL deklaratív nyelv. Ezen írják le a nódok kívánt állapotát, például különböző erőforrások bejelentésével:

  • A fájl létezik, és van meghatározott tartalma.
  • A csomag telepítve van.
  • A szolgáltatás fut.

Az erőforrások összefonódhatnak:

  • Vannak függőségek, amelyek befolyásolják az erőforrások alkalmazásának sorrendjét.
    Például: "először telepítsd a csomagot, aztán módosítsd a konfigurációs fájlt, majd indítsd el a szolgáltatást."
  • Vannak értesítések – ha egy erőforrás megváltozik, értesítéseket küld a légikísérőknek.
    Például, ha a konfigurációs fájl változik, automatikusan újra lehet indítani a szolgáltatást.

Ezen kívül a Puppet DSL-nek vannak funkciói és változói, valamint feltételes operátorai és kiválasztói. Támogatják a különböző sablonmechanizmusokat is – EPP és ERB.

A Puppet Ruby nyelven íródott, ezért sok szerkezet és kifejezés innen származik. A Ruby lehetővé teszi a Puppet kiterjesztését – bonyolult logika és új erőforrástípusok, funkciók hozzáadását.

A Puppet működése során a szerveren minden konkrét nóda mánifesztjei a könyvtárba kerülnek. Mappa — ez az erőforrások és azok kapcsolatai listája, a függvények, változók értékeinek kiszámítása és a feltételes operátorok kibővítése után.

Szintaxis és kódstílus

Itt vannak az hivatalos dokumentáció szakaszai, amelyek segítenek megérteni a szintaxist, ha a megadott példák nem elegendőek:

Itt van egy példa arra, hogyan néz ki a mánifeszt:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

A behúzások és sortörések nem kötelező részét képezik a mánifesztnek, azonban van ajánlott stílusirányelvek.Rövid összefoglaló:

  • Kettős szóköz behúzások, tabulátorok nem használják.
  • A kapcsos zárójeleknek szóköznek kell lenniük, a kettőspontnak nem.
  • Vesszők minden paraméter után, beleértve az utolsót is. Minden paraméter külön sorban. Kivétel az esetben, ha nincs paraméter vagy egyetlen paraméter van: egy sorban és vessző nélkül is írhatsz (pl. resource { 'title': } és resource { 'title': param => value }).
  • A paraméterek nyilait egy szinten kell elhelyezni.
  • Az erőforrások közötti nyilakat előttük kell írni.

Fájlok elhelyezkedése a papetszerveren

A további magyarázatokhoz bevezetek egy fogalmat: "gyökérkönyvtár". A gyökérkönyvtár az a könyvtár, ahol a Puppete konfiguráció található egy adott nodához.

A gyökérkönyvtár verziótól és környezetek használatától függően változik. A környezetek független konfigurációs készletek, amelyeket külön könyvtárakban tárolnak. Általában együtt használják a git-tel, ilyenkor a környezetek git-ágakból készülnek. Ennek megfelelően minden nóda valamilyen környezetben van. Ez a nóda magán, vagy az ENC-ben van beállítva, amiről a következő cikkben mesélek.

  • A harmadik verzióban ("régi Puppeteer") az alapértelmezett könyvtár a /etc/puppet. A környezetek használata opcionális — például mi nem használjuk a régi Puppete-vel. Ha a környezeteket használják, akkor általában a /etc/puppet/environmentskönyvtárban lesz a gyökérkönyvtár. Ha a környezeteket nem használják, a gyökérkönyvtár az alapértelmezett.
  • A negyedik verziótól kezdve ("új Puppeteer") a környezetek használata kötelezővé vált, és az alapértelmezett könyvtárat áthelyezték. /etc/puppetlabs/code. A környezetek így tárolódnak /etc/puppetlabs/code/environments, a gyökérkönyvtár a környezet könyvtára.

A gyökérkönyvtárban lennie kell egy alkönyvtárnak manifests, amelyben egy vagy több manifest található a csomópontok leírásával. Ezen kívül kell lennie egy alkönyvtárnak modules, amelyben a modulok találhatók. A modulok mit is jelentenek, arról később fogok mesélni. Ezen kívül a régi Puppetben is lehet egy alkönyvtár files, amelyben különböző fájlok találhatók, amelyeket a csomópontokra másolunk. Az új Puppetben viszont minden fájl a modulokba került.

A manifest fájlok kiterjesztése .pp.

Egy pár gyakorlati példa

A csomópont és az azon lévő erőforrás leírása

A csomóponton server1.testdomain létre kell hozni egy fájlt /etc/issue a következő tartalommal Debian GNU/Linux n l. A fájlnak a felhasználó és csoport birtokában kell lennie , felhasználó nevében fut, mivel alapvető rendszerbeállításokat módosítanak, de a WordPress közvetlenül egy normál felhasználóként működik., a hozzáférési jogoknak pedig 644.

Manifest írása:

node 'server1.testdomain' {   # a server1.testdomain csomópontra vonatkozó konfigurációs blokk
    file { ' /etc/issue':   # a /etc/issue fájl leírása
        ensure  => present,   # ennek a fájlnak léteznie kell
        content => 'Debian GNU/Linux n l',   # ennek a tartalomnak kell lennie
        owner   => root,   # a tulajdonos felhasználó
        group   => root,   # a tulajdonos csoport
        mode    => '0644',   # a fájlra vonatkozó jogok. Ezeket karakterláncként kell megadni (idézőjelek között), különben a 0-át tartalmazó számot nyolcas számrendszerbeli számként értelmezi a rendszer, és nem úgy alakulnak a dolgok, ahogy terveztük
    }
}

Az erőforrások összefonódása a csomóponton

A csomóponton server2.testdomain el kell indulnia az nginx-nek, amely az előre elkészített konfigurációval dolgozik.

A feladat dekomponálása:

  • Szükség van arra, hogy a csomagot telepítve legyen nginx.
  • Szükség van arra, hogy a konfigurációs fájlok átmásolva legyenek a szerverről.
  • Szükség van arra, hogy a szolgáltatás elinduljon nginx.
  • A konfiguráció frissítése esetén a szolgáltatást újra kell indítani.

Manifest írása:

node 'server2.testdomain' {   # a konfigurációs blokk, amely a server2.testdomain-hez tartozik
    package { 'nginx':   # az nginx csomagot írjuk le
        ensure => installed,   # telepítve kell lennie
    }
  # A közvetlen nyíl (->) azt jelzi, hogy az alább lévő erőforrást
  # az itt leírt erőforrás után kell létrehozni.
  # Az ilyen függőségek áthaladók.
    -> file { '\/etc\/nginx':   # az \/etc\/nginx fájlt írjuk le
        ensure  => directory,   # ennek könyvtárnak kell lennie
        source  => 'puppet:\/\/\/modules\/example\/nginx-conf',   # a tartalmát a megadott címről kell beszerezni a puppetszerverről
        recurse => true,   # fájlok másolása rekurzívan
        purge   => true,   # felesleges fájlokat (amik nincsenek a forrásban) el kell távolítani
        force   => true,   # felesleges könyvtárakat el kell távolítani
    }
  # A hullámos nyíl (~>) azt jelzi, hogy az alább lévő erőforrásnak
  # fel kell iratkoznia a fent leírt erőforrás változásaira.
  # A hullámos nyíl magában foglalja a közvetlen nyilat (->).
    ~> service { 'nginx':   # az nginx szolgáltatást írjuk le
        ensure => running,   # futnia kell
        enable => true,   # automatikusan el kell indítani a rendszer indításakor
    }
  # Amikor egy service típusú erőforrást értesítenek,
  # a megfelelő szolgáltatás újraindul.
}

Ahhoz, hogy ez működjön, körülbelül így kell elhelyezni a fájlokat a puppetszerveren:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Erőforrás típusok

A támogatott erőforrások teljes listája itt található a dokumentációban, itt öt alapvető típust írok le, amelyek a gyakorlatomban elegendőek a legtöbb feladat megoldásához.

file

Fájlok, könyvtárak, szimbolikus linked, azok tartalma, és jogosultságok kezelése.

Paraméterek:

  • az erőforrás neve — a fájl elérési útja (opcionális)
  • path — a fájl elérési útja (ha nem lett megadva a névben)
  • ensure — a fájl típusa:
    • nem létezik — fájl törlése
    • present — bármilyen típusú fájl kell, hogy legyen (ha a fájl nem létezik, egy sima fájl fog létrejönni)
    • file — sima fájl
    • directory — könyvtár
    • link — szimbolikus link
  • content — a fájl tartalma (csak sima fájlokhoz, nem használható együtt a forrás vagy target)
  • forrás — hivatkozás arra az útra, ahonnan a fájl tartalmát másolni kell (nem használható együtt a content vagy target). Megadható URI formájában a bármelyik sémával puppet: (akkor a puppetszerver fájljait fogja használni), így is megadható http: (remélem, ezt így világosan érthető), és még az alábbi sémával file: vagy abszolút útként séma nélkül (akkor a fájl a noden lévő helyi fájlrendszerről lesz kiolvasva)
  • target — hová kell mutatnia a szimlinknek (nem használható együtt a content vagy forrás)
  • tulajdonos — felhasználó, akinek a fájl tulajdonlása tartozik
  • csoport — a fájl, amelyhez a csoport tartozik
  • mód — jogosultságok a fájlra (sztring formájában)
  • rekurzív — rekurszív feldolgozást aktivál
  • töröl — a Puppetben nem leírt fájlok eltávolítását aktiválja
  • kényszerít — a Puppetben nem leírt könyvtárak eltávolítását aktiválja

csomag

Csomagok telepítése és eltávolítása. Képes kezelni az értesítéseket - újratelepíti a csomagot, ha meg van adva a paraméter újratelepítés_frissítéskor.

Paraméterek:

  • az erőforrás neve — a csomag neve (opcionális)
  • name — a csomag neve (ha nincs megadva a névben)
  • szolgáltató — a használni kívánt csomagkezelő
  • ensure — a csomag kívánt állapota:
    • present, telepítve — bármely verzió telepítve van
    • latest — a legújabb verzió telepítve van
    • nem létezik — eltávolítva (apt-get remove)
    • törölve — eltávolítva a konfigurációs fájlokkal együtt (apt-get purge)
    • tartva — a csomag verziója le van zárva (apt-mark hold)
    • bármilyen más sztring — a megadott verzió telepítve van
  • újratelepítés_frissítéskor — ha igaz, akkor az értesítéskor a csomag újra telepítve lesz. Hasznos forrás-alapú disztribúciók esetén, ahol a csomagok újbóli összeállítása szükséges lehet a build paraméterek megváltozása miatt. Alapértelmezés szerint hamis.

a SystemD inicializálási rendszer számára:

Szolgáltatások kezelése. Képes kezelni az értesítéseket - újraindítja a szolgáltatást.

Paraméterek:

  • az erőforrás neve — a szolgáltatás, amelyet kezelni kell (opcionális)
  • name — a szolgáltatás, amelyet kezelni kell (ha nincs megadva a névben)
  • ensure — a szolgáltatás kívánt állapota:
    • fut — fut
    • leállítva — leállítva
  • engedélyez — a szolgáltatás indításának kezelése:
    • igaz — engedélyezett az automatikus indítás (systemctl enable)
    • álarc — el van rejtve (systemctl mask)
    • hamis — le van állítva az automatikus indítás (systemctl disable)
  • újraindít — parancs a szolgáltatás újraindítására
  • status — parancs a szolgáltatás állapotának ellenőrzésére
  • van_újraindítás — megadja, hogy a szolgáltatás init scriptje támogatja-e az újraindítást. Ha hamis és a paraméter meg van adva újraindít — ennek a paraméternek az értéke használatos. Ha hamis és a paraméter újraindít nincs megadva - a szolgáltatás leáll és újraindul az újraindításhoz (de a systemd-ben a parancs systemctl restart).
  • van_állapot — megadni, hogy a szolgáltatás init scriptje támogatja-e a parancsot status. Ha hamis, akkor a paraméter értéke használatos status. Alapértelmezés szerint igaz.

exec

Külső parancsok indítása. Ha nem ad meg paramétereket létrehoz, csak_ha, kivéve vagy csak_frissítéskor, a parancs minden Puppetrun során végrehajtásra kerül. Képes kezelni az értesítéseket - elindítja a parancsot.

Paraméterek:

  • az erőforrás neve – a parancs, amit végre kell hajtani (opcionális)
  • command – a parancs, amit végre kell hajtani (ha nincs megadva a nevében)
  • path – az elérési utak, ahol a végrehajtható fájlt keresik
  • csak_ha – ha a megadott parancs a nullás visszatérési kóddal zárul, a fő parancs végrehajtásra kerül
  • kivéve – ha a megadott parancs a nem nullás visszatérési kóddal zárul, a fő parancs végrehajtásra kerül
  • létrehoz – ha a megadott fájl nem létezik, a fő parancs végrehajtásra kerül
  • csak_frissítéskor — ha igaz, a parancs csak akkor fog elindulni, amikor ez az exec értesítést kap más forrásoktól
  • cwd – az a könyvtár, ahonnan a parancsot futtatni kell
  • user – az a felhasználó, akitől a parancsot futtatni kell
  • szolgáltató – mivel indítjuk a parancsot:
    • posix – csak létrehozza a gyermeket, kötelező megadni path
    • shell – a parancs a shell-ben fut /bin/sh, nem szükséges megadni path, globbingot, pipe-okat és egyéb shell funkciókat lehet használni. Általában automatikusan meghatározza, ha vannak különféle speciális karakterek (|, ;, &&, || stb.).

cron

Kezeli a cron feladatokat.

Paraméterek:

  • az erőforrás neve – csak valami azonosító
  • ensure – a cron feladat állapota:
    • present – létrehozni, ha nem létezik
    • nem létezik – törölni, ha létezik
  • command – milyen parancsot kell futtatni
  • környezet – milyen környezetben kell futtatni a parancsot (a környezeti változók és azok értékei listája =)
  • user – melyik felhasználótól kell futtatni a parancsot
  • perc, óra, hét napja, hónap, hónap napja – mikor kell futtatni a cron-t. Ha bármelyik ezek közül az attribútum közül nincs megadva, annak értéke a crontabban *.

A Puppet 6.0-ban cron mintha eltávolították volna a dobozból a puppetserver-ből, ezért nincs dokumentáció a közös weboldalon. De van a dobozban a puppet-agent-ben, tehát külön telepíteni nem szükséges. A dokumentációját meg lehet nézni az ötödik verziójú Puppet dokumentációjában, vagy a GitHubon.

Az erőforrásokról általánosságban

A források egyediségével szemben támasztott követelmények

A leggyakoribb hiba, amivel találkozunk – Duplikált deklaráció. Ez a hiba akkor fordul elő, amikor egy könyvtárba két vagy több azonos típusú erőforrás ugyanazzal a névvel kerül.

Ezért még egyszer megerősítem: a manifesztumokban egy nód számára nem lehet azonos típusú erőforrást azonos névvel (title)!

Néha szükséges ugyanolyan nevű csomagokat telepíteni, de különböző csomagkezelőkkel. Ilyen esetben a nameparamétert kell használni, hogy elkerüljük a hibát:

csomag { 'ruby-mysql':
  ensure   => telepítve,
  name     => 'mysql',
  provider => 'gem',
}
csomag { 'python-mysql':
  ensure   => telepítve,
  name     => 'mysql',
  provider => 'pip',
}

Más típusú erőforrások esetében hasonló paraméterek találhatók, amelyek segítenek elkerülni a duplikációt – name sa-logic-subsets-canary-vs.yaml a SystemD inicializálási rendszer számára:, command sa-logic-subsets-canary-vs.yaml exec, és így tovább.

Meta-paraméterek

Minden erőforrás típusnak vannak speciális paraméterei, függetlenül a lényegétől.

A meta-paraméterek teljes listája a Puppet dokumentációban.

Rövid lista:

  • require — ebben a paraméterben meg van határozva, hogy mely erőforrásoktól függ ez az erőforrás.
  • before — ebben a paraméterben meg van határozva, hogy mely erőforrások függenek ettől az erőforrástól.
  • subscribe — ebben a paraméterben meg van határozva, hogy mely erőforrásoktól kap értesítést ez az erőforrás.
  • notify — ebben a paraméterben meg van határozva, hogy mely erőforrások kapnak értesítést ettől az erőforrástól.

Minden felsorolt meta-paraméter fogadhat egy erőforrás hivatkozását, vagy egy hivatkozások tömbjét négyzetes zárójelekben.

Hivatkozások erőforrásokra

Az erőforrás hivatkozása egyszerűen egy erőforrás említést jelent. Elsősorban a függőségek megadására használják. A nem létező erőforrás hivatkozása fordítási hibát fog okozni.

A hivatkozás szintaxisa a következő: az erőforrás típusa nagybetűvel (ha a névben kettős kettőspont található, akkor minden részt nagybetűvel kell írni, ami a kettős kettőspontok között helyezkedik el), aztán négyzetes zárójelekben az erőforrás neve (a név kis- és nagybetűszerűsége nem változik!). Nincs hely a hivatkozásban, a négyzetes zárójelek közvetlenül a típusszót követik.

Példa:

file { '/file1': ensure => jelen van }
file { '/file2':
  ensure => könyvtár,
  before => File['/file1'],
}
file { '/file3': ensure => hiányzik }
File['/file1'] -> File['/file3']

Függőségek és értesítések

A dokumentáció itt.

Mint már említettük, az erőforrások közötti egyszerű függőségek tranzitívak. Egyébként ügyeljen a függőségek megadására — ciklikus függőségeket hozhat létre, ami fordítási hibát idézhet elő.

A függőségekkel ellentétben az értesítések nem tranzitívak. Az értesítések esetén a következő szabályok érvényesek:

  • Ha egy erőforrás értesítést kap, frissül. A frissítéskor végrehajtott műveletek típusa az erőforrástól függ — exec parancsot indít el, a SystemD inicializálási rendszer számára: szolgáltatást indít újra, csomag csomagot telepít újra. Ha az erőforráshoz nem tartozik frissítési művelet, akkor semmi sem történik.
  • Egy futás során a Puppet erőforrása legfeljebb egy alkalommal frissül. Ez lehetséges, mivel az értesítések tartalmazzák a függőségeket, és a függőségek grafikonjában nincsenek ciklusok.
  • Ha a Puppet megváltoztatja az erőforrás állapotát, akkor az erőforrás értesítéseket küld minden olyan erőforrásnak, amely fel van iratkozva rá.
  • Ha az erőforrás frissül, akkor értesítéseket küld minden olyan erőforrásnak, amely fel van iratkozva rá.

Nem megadott paraméterek kezelése

Általában, ha egy erőforrás paraméterének nincs alapértelmezett értéke, és ez a paraméter nincs megadva a manifestben, a Puppet nem fogja megváltoztatni ezt a tulajdonságot a megfelelő erőforráson a node-on. Például, ha egy erőforrás típusa file nincs megadva a paraméter tulajdonos, akkor a Puppet nem fogja megváltoztatni a megfelelő fájl tulajdonosát.

Megismerkedés osztályokkal, változókkal és definíciókkal

Tegyük fel, hogy van néhány node-unk, amelyek azonos konfigurációs részekkel rendelkeznek, de vannak különbségek is – különben mindent leírhatnánk egy blokkon belül. node {}. Természetesen egyszerűen lemásolhatjuk az azonos konfigurációs részeket, de általában ez rossz megoldás – a konfiguráció nő, és ha a közös konfigurációs rész megváltozik, akkor ugyanazt számos helyen kell javítani. Emellett könnyű hibázni, és egyáltalán, a DRY (don’t repeat yourself) elv nem véletlenül vált népszerűvé.

Egy ilyen probléma megoldására létezik egy konstrukció, mint osztály.

Osztályok

Osztály — ez egy nevén nevezett blokk a Puppet kódban. Az osztályok a kód újrahasználásához szükségesek.

Először az osztályt le kell írni. Maga a leírás nem ad hozzá semmilyen erőforrást. Az osztályokat a manifestelemekben írják le:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Ezután az osztályt fel lehet használni:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Előző feladat példája – különítsük el az nginx telepítését és beállítását egy osztályba:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => 'puppet:///modules/example/nginx-conf',
        recurse => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

Változók

Az előző példából származó osztály teljesen rugalmatlan, mivel mindig ugyanazt az nginx konfigurációt hozza. Tegyünk úgy, hogy a konfigurációs útvonal változóvá váljon, így ezt az osztályt bármilyen konfigurációjú nginx telepítésére felhasználhatjuk.

Ezt meg lehet tenni változók segítségével.

Figyelem: a Puppet változók immutable-ek!

Továbbá, egy változóhoz hozzáférni csak akkor lehet, miután deklarálták, különben a változó értéke undef.

Példa a változók használatára:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

A Puppet-ben vannak névterek, és a változóknak ennek megfelelően van hatókörük: egy változó ugyanazzal a névvel különböző névterekben van definiálva. A változó értékének meghatározásakor a változót a jelenlegi névtérben keresik, majd a külsőben, és így tovább.

Néhány névtér példa:

  • globális — ide tartoznak a változók, amelyek a osztály vagy csomópont leírásán kívül helyezkednek el;
  • csomópont névtér a csomópont leírásában;
  • osztály névtér az osztály leírásában.

A változóra való hivatkozás ambiguitásának elkerülése érdekében meg lehet határozni a névteret a változó nevében:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Tegyük fel, hogy az nginx konfiguráció elérési útja a változóban található $nginx_conf_source. Így a osztály a következőképpen néz ki:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { 'etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # itt a váltó nevet használjuk a fix szöveg helyett
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    $nginx_conf_source = 'puppet:///modules/example/nginx-conf'
    include nginx_example
}

Azonban a fenti példa rossz, mert van egyfajta „titkos tudás” arról, hogy a osztályon belül valahol egy változót használunk ezzel a névvel. Sokkal helyesebb ezt a tudást közkinccsé tenni — az osztályoknak lehetnek paramétereik.

Osztály paraméterek — ezek a változók az osztály névterében találhatók, a osztály fejléce alatt határozzák meg őket, és szokásos változókként használhatók a osztály törzsében. A paraméterek értékeit a osztály manifestben történő használatakor adják meg.

A paraméternek be lehet állítani egy alapértelmezett értéket. Ha a paraméternek nincs alapértelmezett értéke, és az értéket nem adják meg a használatkor, fordítási hibát vált ki.

Parametrizáljuk az előző példa osztályt, és adjunk hozzá két paramétert: az első, kötelező — a konfiguráció elérési útja, és a második, opcionális — az nginx csomag neve (Debianban például léteznek csomagok nginx, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

A Puppet-ben a változók típusosak. Vannak sokféle adat típusok. Az adattípusokat általában a paraméterek értékeinek validálására használják, amelyek a osztályokba és definiálásokba kerülnek. Ha a megadott paraméter nem felel meg a megadott típusnak, fordítási hiba lép fel.

A típus közvetlenül a paraméter neve elé kerül:

class example (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Osztályok: include classname vs class{‘classname’:}

Mindegyik osztály erőforrás típusú class. Ahogyan más erőforrástípusok esetén, egy node-on nem lehet két azonos osztály példány.

Ha megpróbáljuk ugyanazt az osztályt kétszer hozzáadni ugyanahhoz a node-hoz a class { 'classname':} (függetlenül attól, hogy eltérő vagy azonos paraméterekkel), fordítási hiba lép fel. Azonban ha az osztály erőforrás stílusban van használva, akkor a manifestben az összes paramétert azonnal megadhatjuk.

Ha azonban használja a include, akkor az osztályt annyiszor lehet hozzáadni, ahányszor csak akarjuk. A lényeg az, hogy include — idempotens funkció, amely ellenőrzi, hogy az osztály hozzá lett-e adva a könyvtárhoz. Ha az osztály nincs a könyvtárban — hozzáadja, ha már létezik, semmit sem csinál. De ha az include használatakor nem lehet megadni az osztály paramétereit az osztály bejelentése során — az összes kötelező paramétert külső adatforrásban kell megadni — Hiera vagy ENC. Ezekről a következő cikkben beszélünk.

Definíciók

Ahogyan az előző blokkban említésre került, ugyanaz az osztály nem lehet jelen egy node-on többször. Bizonyos esetekben azonban szükség van arra, hogy ugyanazt a kódblokkot különböző paraméterekkel alkalmazzuk egy node-on. Más szóval, szükség van egy saját erőforrás típusra.

Például, ha PHP modult szeretnénk telepíteni, mi az Aviton a következőt tesszük:

  1. Telepítjük a csomagot ezzel a modullal.
  2. Konfigurációs fájlt hozunk létre ehhez a modulhoz.
  3. Létrehozunk egy symlinket a php-fpm konfigurációhoz.
  4. Létrehozunk egy symlinket a php cli konfigurációhoz.

Ezekben az esetekben olyan konstrukciót használunk, mint definíció (define, defined type, defined resource type). A definíció hasonló az osztályhoz, de van néhány különbség: először is, minden definíció erőforrás típus, nem erőforrás; másodszor, minden definíciónak van egy implicit paramétere, $title, ahová a forrás neve kerül az bejelentés során. Ahogyan az osztályok esetében, a definíciót is először le kell írni, majd használni lehet.

Egyszerű példa PHP modul használatával:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = 'etc/php/7.4/mods-available',
) {
  package { $php_package_name:
    ensure          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # a debianos php csomagok saját maguk hoznak létre szimlinkeket és indítják újra a php-fpm szolgáltatást - erre nekünk nincs szükségünk, mivel a szimlinkeket és a szolgáltatást Puppet segítségével kezeljük
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    ensure  => $ensure,
    content => $data,
  }
  file { "etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

node server3.testdomain {
  php74::module { 'sqlite3': }
  php74::module { 'amqp': php_package_name => 'php-amqp' }
  php74::module { 'msgpack': priority => '10' }
}

A define-ben a legkönnyebb elkapni a Duplicate declaration hibát. Ez akkor fordul elő, ha a definícióban van egy erőforrás állandó névvel, és valahol két vagy több példány van ebből a definícióból.

Emiatt könnyen védekezhetünk: minden erőforrás, ami a definícióban van, olyan nevet kell kapjon, ami attól függ, $title. Alternatívaként - idempotens erőforrás hozzáadás, ami a legegyszerűbb esetben elegendő, ha az összes példány számára közös erőforrásokat egy külön osztályba emeljük, és ezt az osztályt inkludáljuk a definícióba - a funkció include idempotens.

Más módszerek is léteznek az idempotencia elérésére erőforrások hozzáadásánál, ezek közé tartozik a defined és ensure_resources, de erről a következő részből fogok beszélni.

Osztályok és definíciók függőségei és értesítései

Az osztályok és definíciók a következő szabályokat adják a függőségek és értesítések kezeléséhez:

  • osztály/definíció függőségei minden erőforrás függőségét hozzáadják az osztályhoz/definícióhoz;
  • osztály/definíció függősége minden erőforrást a class/define-hoz fűz;
  • az osztály/definíció értesítése értesít minden erőforrást az osztályban/definícióban;
  • osztály/definíció feliratkozás minden erőforrást feliratkoztat az osztályban/definícióban.

Feltételes operátorok és szelektorok

A dokumentáció itt.

if

Ez itt nagyon egyszerű:

if KIFEJEZÉS1 {
  ...
} elsif KIFEJEZÉS2 {
  ...
} else {
  ...
}

kivéve

unless - ez az if ellentéte: a kód blokk akkor fut le, ha a kifejezés hamis.

unless KIFEJEZÉS {
  ...
}

case

Itt semmi bonyolult nincs. A végervényeket használhatunk: bíztos értékek (stringek, számok és így tovább), reguláris kifejezések, és adathordozó típusok.

case KIFEJEZÉS {
  ÉRTÉK1: { ... }
  ÉRTÉK2, ÉRTÉK3: { ... }
  default: { ... }
}

Kiválasztók

A kiválasztó egy nyelvi konstrukció, amely hasonlít a case, de a kódrészlet végrehajtása helyett értéket ad vissza.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

Modulok

Amikor a konfiguráció kicsi, könnyen kezelhető egyetlen manifestben. De minél több konfigurációt írunk le, annál több osztály és elem kerül a manifestbe, és egyre nehezebben kezelhetővé válik.

Ezen kívül van a kód újrafelhasználásának problémája - amikor az összes kód egyetlen manifestben található, nehezen osztható meg másokkal. E két problémára a Puppetben van egy entitás, amit moduloknak hívnak.

Modulok – ezek osztályok, definíciók és egyéb Puppet-entitások készletei, amelyek egy külön könyvtárba vannak kiemelve. Más szóval, a modul egy független darab Puppet-logika. Például lehet egy modul az nginx kezelésére, ami csak az ahhoz szükséges dolgokat tartalmazza, és lehet egy modul a PHP kezelésére, és így tovább.

A modulok verziózva vannak, és támogatják egymás moduljainak függőségeit. Van egy nyílt modulrepositoriohűv Puppet Forge.

A puppet-szerveren a modulok a gyökérkönyvtár modules almappájában találhatók. Minden modulon belül a standard mappaszerkezet - manifests, files, templates, lib és így tovább.

A modul fájlstruktúrája

A modul gyökerében a következő, beszédes nevű mappák találhatók:

  • manifests – itt találhatók a manifesztok
  • files – itt találhatók a fájlok
  • templates – itt találhatók a sablonok
  • lib – itt található a Ruby-kód

Ez nem teljes listája a könyvtáraknak és fájloknak, de ennek a cikknek elegendő.

Az erőforrások nevei és a fájlnevek a modulban

A dokumentáció itt.

Az erőforrásokat (osztályok, definíciók) a modulban nem lehet tetszőlegesen elnevezni. Ezenkívül közvetlen megfelelés van a forrás neve és a fájl neve között, amelyben a Puppet keresni fogja ennek az erőforrásnak a leírását. Ha megsértjük a névadási szabályokat, a Puppet egyszerűen nem találja meg az erőforrások leírását, és fordítási hibát okoz.

A szabályok egyszerűek:

  • Minden erőforrásnak a modul névtérben kell lennie. Ha a modul neve foo, akkor az összes erőforrásnak így kell neveződnie: foo::, vagy egyszerűen csak foo.
  • A modul nevével rendelkező erőforrásnak a következő fájlban kell lennie: init.pp.
  • A többi erőforrás esetén a fájlnevek elnevezési séma a következő:
    • a modul nevét tartalmazó előtag elhagyásra kerül.
    • minden kettős kettőspontot, ha van, perjelre cserélnek
    • kiterjesztés kerül hozzárendelésre .pp

Példával fogom bemutatni. Tegyük fel, hogy írok egy modult nginx. Ebben a következő erőforrások találhatók:

  • osztály nginx a manifestben van leírva init.pp;
  • osztály nginx::service a manifestben van leírva service.pp;
  • definíció nginx::server a manifestben van leírva server.pp;
  • definíció nginx::server::location a manifestben van leírva server/location.pp.

Sablonok

Valószínűleg jól tudja, mi az a sablon, ezért nem fogom itt részletezni. De minden esetre itt hagyok egy hivatkozást a Wikipédiára.

Hogyan használjunk sablonokat: a sablon értékét a template, amelynek átadjuk a sablonhoz vezető utat. Olyan erőforrások esetén, mint a file együtt kell használni a paraméterrel content. Például így:

file { '/tmp/example': content => template('modulename/templatename.erb')

Az alábbi formátumú útvonal / azt jelenti, hogy a fájl /modules//templates/.

Ezen kívül van egy inline_template — ehhez a sablon szövegét adja meg, nem a fájl nevét.

A sablonokban használhatók az összes Puppet változó az aktuális láthatósági tartományban.

A Puppet támogatja az ERB és EPP formátumú sablonokat:

Röviden az ERB-ről

vezérlő konstrukciók:

  • <%= ВЫРАЖЕНИЕ %> — a kifejezés értékének beszúrása
  • <% ВЫРАЖЕНИЕ %> — a kifejezés értékének kiszámítása (anélkül, hogy beszúrná). Ide tartoznak a szokásos feltételes operátorok (if), ciklusok (each).
  • <%# КОММЕНТАРИЙ %>

A kifejezések az ERB-ben Ruby-ban íródnak (tulajdonképpen az ERB az Embedded Ruby).

A manifestből való változók eléréséhez hozzá kell adni @ a változó nevéhez. A vezérlő konstrukció után megjelenő új sort a záró tag használatával lehet eltüntetni -%>.

A sablon használatának példája

Tegyük fel, hogy írok egy modult a ZooKeeper kezelésére. A konfiguráció létrehozásáért felelős osztály körülbelül így néz ki:

class zookeeper::configure (
  Array[String] $nodes,
  Integer $port_client,
  Integer $port_quorum,
  Integer $port_leader,
  Hash[String, Any] $properties,
  String $datadir,
) {
  file { '/etc/zookeeper/conf/zoo.cfg':
    ensure => present,
    content => template('zookeeper/zoo.cfg.erb'),
  }
}

A megfelelő sablon zoo.cfg.erb — így:

0 -%>

server.=::;



dataDir=


=

Tények és beépített változók

Gyakran a konfiguráció egy konkrét része attól függ, hogy mi történik éppen a noden. Például a Debian aktuális kiadásától függően szükség van a megfelelő csomagverzió telepítésére. Mindez manuálisan is nyomon követhető, a node-k változásakor újra kell írni a manifesztusokat. De ez nem egy komoly megközelítés, az automatizálás sokkal jobb.

A nodok információinak megismerésére a Puppet-ben van egy mechanizmus, amit tényeknek hívnak. Tények a node-ra vonatkozó, a manifesztusokban globális változókként elérhető információ. Például a gazdagép neve, az operációs rendszer verziója, a processzor architektúrája, a felhasználók listája, a hálózati interfészek és azok címének listája, és még sok más. A tények a manifesztusokban és a sablonokban szokásos változókként elérhetőek.

Példa a tények használatára:

notify { "Futó OS ${facts['os']['name']} verzió ${facts['os']['release']['full']}" : }
# A notify típusú erőforrás egyszerűen egy üzenetet ír ki a naplóba.

Formálisan szólva, a ténynek van egy neve (string) és egy értéke (különböző típusok elérhetőek: stringek, tömbök, szótárak). Van egy beépített tények halmaza. Emellett saját tényeket is lehet írni. A ténygyűjtők Ruby nyelven funkciókként vannak leírva, vagy végrehajtható fájlokként. A tények megjelenhetnek szöveges adatfájlokban a nodon.

A Puppet ügynök működése közben először lemásolja a Puppet szerverről az összes elérhető ténygyűjtőt a node-ra, majd futtatja őket és elküldi a szervernek a begyűjtött tényeket; ezt követően a szerver megkezdi a katalógus összeállítását.

Végrehajtható fájlok formájában lévő tények

Ezek a tények a modulokban a facts.dkönyvtárba kerülnek. Nyilvánvalóan a fájlok végrehajthatóknak kell lenniük. Futáskor a standard kimenetre kell kibővíteniük információt vagy YAML, vagy "kulcs=érték" formátumban.

Ne feledje, hogy a tények minden node-ra kiterjednek, amelyek a Puppet szerver irányítása alatt állnak, ahová a modulját telepíti. Ezért ügyeljen a szkriptben arra, hogy a rendszerben minden a tény működéséhez szükséges program és fájl megtalálható legyen.

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Ruby nyelvű tények

Ezek a tények a modulokban a lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Szöveges tények

Ezek a tények a nodon a /etc/facter/facts.d régi Puppet-ben vagy /etc/puppetlabs/facts.d új Puppet-ben.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

A tények elérése

A tényekhez kétféleképpen lehet hozzáférni:

  • szótáron keresztül $facts: $facts['fqdn'];
  • a tény nevét használva mint változónevet: $fqdn.

A legjobban a szótár használata ajánlott $facts, és még jobb, ha globális névtér-t használsz ($::facts).

Itt van a szükséges dokumentációs rész.

Beépített változók

A tényeken kívül még vannak más változók, amelyek elérhetőek a globális névtérben.

  • trusted facts — olyan változók, amelyek az ügyfél tanúsítványából származnak (mivel a tanúsítványt általában a puppet-szerveren állítják ki, az ügynök nem tudja csak úgy megváltoztatni a saját tanúsítványát, ezért ezek a változók "megbízhatóak": tanúsítvány neve, gazdagép neve és domain neve, a tanúsítvány kiterjesztései.
  • server facts — a szerverrel kapcsolatos információkat tartalmazó változók — verzió, név, IP-cím, környezet.
  • agent facts — a puppet-agent által közvetlenül hozzáadott változók, nem a facter által — tanúsítvány név, ügynök verzió, puppet verzió.
  • master variables — a puppetmaster változói (sic!). Hasonlóak a fentiekhez, server facts, plusz elérhetőek a konfigurációs paraméterek értékei.
  • compiler variables — a fordító változói, amelyek minden láthatóságban eltérnek: a jelenlegi modul neve és a modul neve, ahonnan a jelenlegi objektumra hivatkoztak. Ezeket például arra lehet használni, hogy ellenőrizzék, hogy a privát osztályaik ne legyenek közvetlenül más modulokból használva.

Kiegészítés 1: hogyan indítsuk és hibakeressük mindezt?

A cikkben sok példa volt a puppet-kódra, de arról nem volt szó, hogyan lehet ezt a kódot futtatni. Nos, korrigálom magam.

A Puppet működéséhez elegendő az ügynök, de a legtöbb esetben szükség lesz a szerverre is.

Ügynök

Legalább az ötödik verziótól kezdve a puppet-agent csomagok a Puppetlabs hivatalos repójából minden szükséges függőséget tartalmaznak (ruby és a megfelelők gem-ek), ezért telepítési problémák nem állnak fenn (Debian-alapú disztribúciókra beszélek — az RPM-alapú disztribúciókat nem használjuk).

A legegyszerűbb esetben a puppet-konfiguráció alkalmazásához elegendő az ügynököt futtatni süket szerver módba: feltételezve, hogy a puppet-kód a nodára lett másolva, futtatni kell puppet apply:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Helló világ!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Értesítés: Összeállított katalógus az atikhonov.localdomain számára az production környezetben 0.01 másodperc alatt
Értesítés: Helló világ!
Értesítés: /Stage[main]/Main/Node[default]/Notify[Helló világ!]/message: a 'message' definálva: 'Helló világ!'
Értesítés: Katalógus alkalmazva 0.01 másodperc alatt

Természetesen jobb egy szervert indítani és a nodokon ügynököket daemon módban futtatni — ekkor félóránként alkalmazni fogják a szerverről letöltött konfigurációt.

Lehet szimulálni a push-modell működését — lépjen be a kívánt nodera és indítsa el sudo puppet agent -t. A kulcs - folyamatosan „pingelni” (a leállításhoz nyomjuk meg a Ctrl+C kombinációt) (--test) valójában több opciót tartalmaz, amelyeket külön is bekapcsolhat. Ezek között a következők találhatók:

  • nem működik daemon módban (alapértelmezés szerint az ügynök daemon módban indul);
  • a katalógus alkalmazása után leáll (alapértelmezés szerint az ügynök folytatja a működést, és félóránként alkalmazza a konfigurációt);
  • részletes működési naplót ír;
  • mutatja a fájlokban bekövetkezett változásokat.

Az ügynöknek van egy változatlan üzemmódja — ezt akkor lehet használni, ha nem biztos abban, hogy helyes konfigurációt írt, és szeretné ellenőrizni, pontosan mit változtat az ügynök a működés során. Ezt az üzemmódot a parancssorban a következő paraméter kapcsolja be: --noop : sudo puppet agent -t --noop.

Továbbá kapcsolhat be debug naplózást is — ebben a puppet minden műveletéről ír, amelyet végrehajt: az éppen feldolgozott erőforrásról, annak paramétereiről, arról, hogy milyen programokat indít. Természetesen ez a paraméter --debug.

Szerver

A puppetserver teljes körű beállítása és a kód telepítése nem a cikk témája, csak annyit mondok, hogy gyári állapotban egy jól működő verzió települ, amely nem igényel további konfigurációt a kis mennyiségű nod működtetéséhez (mondjuk százig). Nagyobb számú nod már finomhangolást igényel — alapértelmezés szerint a puppetserver nem indít el többet négy munkásnál, a nagyobb teljesítményhez növelni kell a számukat, és nem szabad elfelejteni a memóriahatárokat növelni, különben a szerver többségében garbage collect’eli az időt.

A kód telepítése — ha gyorsan és egyszerűen akarja, nézze meg (r10k)[https://github.com/puppetlabs/r10k], a kis telepítésekhez ez bőven elegendő kell, hogy legyen.

Kiegészítés 2: Kódírási ajánlások

  1. Minden logikát helyezzen el osztályokban és definíciókban.
  2. Tartsuk a classokat és definíciókat modulokban, ne pedig a csomópontok leírásában található manifestekben.
  3. Használja a tényeket.
  4. Ne készítsen if-utasításokat hosztnevek alapján.
  5. Ne habozzon hozzáadni paramétereket a classokhoz és definíciókhoz - ez jobb, mint a rejtett logika a class/definíció testében.

Amiért javaslom ezt, azt a következő cikkben elmagyarázom.

Zárás

Ezzel befejeztük az bevezetőt. A következő cikkben a Hieráról, az ENC-ről és a PuppetDB-ről fogok beszélni.

Csak a regisztrált felhasználók vehetnek részt a szavazáson. Kérem, jelentkezzen be., kérem.

Valójában sokkal több anyag van - tudok cikkeket írni a következő témákban, szavazzanak, hogy miről szeretnének olvasni:

  • 59,1%Fejlett Puppet konstruktumok - néhány következő szintű dolog: ciklusok, leképezések és egyéb lambda kifejezések, erőforrásgyűjtők, exportált erőforrások és hosztok közötti interakció Puppet-en keresztül, címkék, szolgáltatók, absztrakt adattípusok.
  • 31,8%» Én a mamának vagyok admin« vagy hogyan barátkoztunk össze több különböző verziójú Puppet-szerverrel az Avitónál, nos, és általában egy rész a Puppet-szerver adminisztrálásáról.
  • 81,8%Hogyan írjuk a Puppet kódot: eszközláncolás, dokumentáció, tesztelés, CI/CD.

22 felhasználó szavazott. 9 felhasználó tartózkodott.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster