Tässä webinaaritallenteessa esittelemme perusteet infrastruktuurin rakentamisesta koodilla, mukaan luettuna versionhallinnan, laadunvarmistuksen ja erilaiset työkalut kuten Puppetin, Terraformin, Ansiblen ja Puppet Boltin:

Toteutettu yhteistyössä Turku Business Regionin kanssa 5.5.2020.

Kun käytetään r10k:ta ja Foremania yhdessä on uusien luokkien tuonti kaksivaiheinen prosessi. Oletetaan vaikka, että "production" environmentiin on luotu uusi luokka, esim. "role::ipa_server". Ilman Foremania riittäisi tuoda muutos puppetmasterille r10k:lla:

$ r10k deploy environment production -vp

Foreman ei kuitenkaan vielä tiedä uudesta luokasta mitään:

$ hammer puppet-class list --environment production|grep ipa_server

Foremanin komponentti, joka hakee Puppetmasterilta uudet luokat on nimeltään "Smart proxy", joka tunnetaan myös nimellä "foreman-proxy". Uusimpien Puppetin versioiden kanssa se hakee luokat Puppetserverin API:a käyttäen. Luokat saa päivitettyä komentoriviltä melko triviaalisti:

$ hammer proxy import-classes --id 1
Result:
Successfully updated environment and puppetclasses from the on-disk puppet installation
Changed environments:
1) production
New classes:
role::ipa_server

Yllä "id" viittaa smart proxyyn, jolta puppet-luokat päivitetään (ks. "hammer proxy list").

Edellä mainittu komento ei kuitenkaan tee mitään jos Puppetserverin environment class cache on päällä, koska smart proxy saa API-kyselyn tehdessään vanhentuneen listan Puppet-luokista. Ongelman voi korjata poistamalla ko. ominaisuuden käytöstä tiedostosta /etc/puppetlabs/puppetserver/conf.d/puppetserver.conf:

# (optional) enable or disable environment class cache
environment-class-cache-enabled: false

Tämän jälkeen smart proxyn logeihin ilmestyykin varoitus:

2019-08-08T12:12:21 ed5f29fe [W] Puppet server classes cache is disabled, classes retrieval can be slow.

Tämä ei kuitenkaan ole mikään ongelma ainakaan pienehköissä ympäristöissä ja vähentää huomattavasti harmaiden hiuksien määrää tehtäessä nopeaa Puppet-koodin kehitystä.

Toisinaan käy niin, että on tarpeen varmistua siitä aiheuttaako iso kasa Git-committeja jotain sivuvaikutuksia esimerkiksi Puppet-tuotantoympäristössä. Hyvistä commit-viesteistä on aina toki apua, mutta toisinaan commitit eivät ole täysin atomisia, eli tekevät enemmän kuin yhden (dokumentoidun) asian. Lisäksi esimerkiksi Puppet control reposta saattaa löytyä paitsi Puppet-koodia, myös Terraformia ja Ansiblea. Tälläisessä tapauksessa on kätevää näyttää vain muutokset, jotka ovat oikeasti relevantteja. Ensin tarkistetaan, mikä "production"-ympäristön viimeisin commit on:

$ ssh puppet
$ cd /etc/puppetlabs/code/environments/production
$ git rev-parse --short HEAD
86c2152

Tämän jälkeen voidaan tarkista control reposta muutokset Puppetin kannalta oleellisiin kansioihin ja tiedostoihin:

$ cd puppet-control-repo
$ git diff 86c2152…HEAD -- data manifests site Puppetfile

Näin ei tarvitse kahlata läpi myös niitä committeja, jotka eivät mitenkään voi liittyä varsinaiseen Puppet-koodiin ja eivät siten voi aiheuttaa ongelmia tuotantokoneilla.

Pitkällisen kehitystyön tuloksena saimme kuin saimmekin tehtyä Puppetmasterin Kafo-pohjaisesta asentimestamme oikean AWS Marketplace-tuotteen:

Tuotteen hinnoittelu perustuu käyttötunteihin. Tuote tukee kolmea eri skenaariota:

Installer tekee aiemmin jokseenkin rasittavasti Puppetmasterien asentelusta suorastaan naurettavan helppoa:

$ ssh -i ~/.ssh/mykey.pem [email protected]
 $ sudo -i
 $ puppetmaster-installer -i

Tämän jälkeen valitaan skenaario ja mahdollisesti muutetaan joitain parametreja ja ajetaan asennin. PuppetDB:n ja Puppetboardin salasanat luodaan automaattisesti jos niitä ei ole määritetty.

Installerin voi ajaa myös automaattisesti jos Puppetmaster provisioidaan esimerkiksi Terraformilla. Näin voidaan luoda Puppet-palvelinympäristöjä ilman käsityötä.

Puppet-moduulien dokumentaation saa helposti päivitettyä "puppet strings"-työkalulla, jonka käyttö yleisellä tasolla on dokumentoitu varsin hyvin. Kun komento vielä yhdistetään Gitin pre-commit -hookiin, saadaan dokumentaatio päivitettyä automaattisesti joka commitilla. Alla näytetään, miten tämä saadaan toteutettua niin Linuxissa kuin Windowsissa. Oletuksena on, että molemmissa tapauksissa Puppet Agent 5.x on asennettu virallisista Puppetlabsin paketeista.

Aloitan helpommasta eli Linuxista käyttäen esimerkkinä Debian 9:iä: ohjeiden pitäisi kuitenkin toimia myös Ubuntuissa aivan samalla tavoin. Ensin asennetaan Git:

$ sudo -s
 $ apt-get update && apt-get install git

Sen jälkeen asennetaan tarvittavat gemmit käyttäen Puppetin gem-komentoa:

$ /opt/puppetlabs/puppet/bin/gem install yard puppet-strings

Tämän jälkeen mennään dokumentoitavan moduulin juureen:

$ cd ~/opt/puppeteers/puppet-puppetmaster

Tarkistetaan, että "puppet strings" toimii:

$ mkdir docs
 $ /opt/puppetlabs/bin/puppet strings generate --format markdown --out docs/INFO.md
 [warn]: Missing @param tag for parameter 'foreman_db_password' near manifests/lcm.pp:149. 
 
 --- snip ---
 
 manifests/foreman_proxy.pp:87. [0/1899]
 [warn]: Missing @param tag for parameter 'timezone' near manifests/foreman_proxy.pp:87. 
 Files: 8 
 Modules: 0 ( 0 undocumented) 
 Classes: 0 ( 0 undocumented) 
 Constants: 0 ( 0 undocumented) 
 Attributes: 0 ( 0 undocumented) 
 Methods: 0 ( 0 undocumented) 
 Puppet Classes: 7 ( 0 undocumented) 
 Puppet Defined Types: 1 ( 1 undocumented) 
 Puppet Types: 0 ( 0 undocumented) 
 Puppet Providers: 0 ( 0 undocumented) 
 Puppet Functions: 0 ( 0 undocumented) 
 Puppet Tasks: 0 ( 0 undocumented) 
 Puppet Plans: 0 ( 0 undocumented) 
 87.50% documented

Kuten yllä näkyy, puppet-puppetmaster-moduuli voisi olla paremminkin dokumentoitu, ainakin puppet stringsin mielestä. Joka tapauksessa tiedostosta docs/INFO.md löytyy nyt Markdown-muotoinen dokumentaatio moduulista. Jäljellä on siis enää dokumentaation luonnin ja muutosten commitoinnin automatisointi Gitin pre-commit hookilla. Tämä onnistuu luomalla tiedosto .git/hooks/pre-commit seuraavalla sisällöllä:

#!/bin/sh
 #
 # Generate module documentation in markdown format and add it
 # automatically to the commit
 /opt/puppetlabs/puppet/bin/puppet strings generate --format markdown 
 --out docs/INFO.md
 
 git add docs/INFO.md

Määritetään tiedosto käynnistyskelpoiseksi:

$ chmod 755 .git/hooks/pre-commit

Lopuksi tarkistetaan toimiiko pre-commit hook. Muokataan ensin jonkin luokan dokumentaatiota, ajetaan sille "git add" ja sen jälkeen "git commit". Kun commit on tehty, voidaan tarkistaa miten kävi:

commit 1ce00aa38fcc13471390a25c83f7c6ea2d59c430
 Author: Samuli Seppänen <[email protected]>
 Date: Tue Nov 20 10:09:58 2018 +0200
 
 Test pre-commit hook
 
 diff --git a/docs/INFO.md b/docs/INFO.md
 index da94769..4115ffd 100644
 --- a/docs/INFO.md
 +++ b/docs/INFO.md
 @@ -5,7 +5,7 @@
 
 **Classes**
 
 -* [`puppetmaster::common`](#puppetmastercommon): Common configurations for all scenarios as you see
 +* [`puppetmaster::common`](#puppetmastercommon): Common configurations for all scenarios as you see foobar
 * [`puppetmaster::common::r10k`](#puppetmastercommonr10k): r10k install and configure, but not run
 * [`puppetmaster::foreman_proxy`](#puppetmasterforeman_proxy): Class to setup Foreman smart proxy == Parameters: $foreman_proxy_foreman_base_url:: XXX $foreman_proxy_templates:: XXX $foreman_proxy_tem
 * [`puppetmaster::lcm`](#puppetmasterlcm): Class to setup Foreman with a Smart Proxy == Parameters: $foreman_db_password:: The password for the foreman database. $foreman_admin_firs
 @@ -21,7 +21,7 @@
 
 ### puppetmaster::common
 
 -Common configurations for all scenarios as you see
 +This class includes common configurations for all scenarios
 
 #### Parameters
 
 diff --git a/manifests/common.pp b/manifests/common.pp
 index df99c02..3c4bd9a 100644
 --- a/manifests/common.pp
 +++ b/manifests/common.pp
 @@ -1,4 +1,4 @@
 -# Common configurations for all scenarios
 +# This class includes common configurations for all scenarios 
 class puppetmaster::common
 (
 Array[String] $primary_names,

Stringsin luomaan markdown-tiedosto on helpointa pitää täysin erillään README.md:stä ja viitata siihen tähän tyyliin:

 Module documentation is available in [docs/INFO.md](docs/INFO.md).

Windowsin tapauksessa homma toimii lähes identtisesti muutamaa lisäkikkaa lukuun ottamatta. Alla oletetaan, että pakettien asennukseen on käytettävissä Chocolatey. Ensin käynnistetään Powershell admin-oikeuksin ja asennetaan Git:

PS> choco install -y git

Haluattaessa voidaan asentaa jokin järkevä tekstieditorikin, esim. Notepad++:

PS> choco install -y notepadplusplus

Seuraavaksi asennetaan puppet-strings Puppetin gemmillä:

PS> cd 'C:Program FilesPuppet LabsPuppetsysrubybin'
 PS> .gem install yard puppet-strings
 Fetching: yard-0.9.16.gem (100%)
 --- snip ---
 Successfully installed yard-0.9.16
 Parsing documentation for yard-0.9.16
 Installing ri documentation for yard-0.9.16
 Done installing documentation for yard after 12 seconds
 Fetching: rgen-0.8.2.gem (100%)
 Successfully installed rgen-0.8.2
 Fetching: puppet-strings-2.1.0.gem (100%)
 Successfully installed puppet-strings-2.1.0
 Parsing documentation for rgen-0.8.2
 Installing ri documentation for rgen-0.8.2
 Parsing documentation for puppet-strings-2.1.0
 Installing ri documentation for puppet-strings-2.1.0
 Done installing documentation for rgen, puppet-strings after 8 seconds
 3 gems installed

Sitten käynnistetään admin Powershell uudelleen, jotta saadaan järjestelmän polku päivitettyä ja siirrytään dokumentoitavan moduulin juureen:

PS> cd C:UsersSamulipuppet-puppetmaster

Seuraavaksi kirjoitetaan pre-commit skripti (".githookspre-commit.ps1") Powershellillä:

# Place this script to
 #
 # <module-dir>.githookspre-commit.ps1
 #
 puppet strings generate --format markdown --out docsINFO.md
 git add docs/INFO.md

Koska Git ei suoraan tue mitään muita kuin Bash-skriptejä, ei se aja em. skriptiä ilman ohutta Bash-wrapperiä (".githookspre-commit"):

#!C:/Program Files/Git/usr/bin/sh.exe
 #
 # This script goes to
 #
 # <module-dir>.githookspre-commit
 #
 exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "..githookspre-commit.ps1"

Bash-wrapper käyttää Gitin Windows-paketin mukana tulevaa Bashia, jonka voi yleensä olettaa olevan asennettuna jos Windowissa on ylipäätään Git.

Tämän jälkeen pre-commit hook toimii aivan samoin kuin Linuxissakin.

Linuxissa palomuurina - tai tarkemmin pakettisuodattimena - käytetään netfilteriä, jota hallitaan yleensä iptables (IPv4) ja ip6tables (IPv6) komennoilla. Iptables-sääntöjen määritys Puppetissa onnistuu helpoiten puppetlabs/firewall -moduulilla. Tyypillinen käyttötapaus on portin avaaminen palomuuriin; tästä tyypillisenä esimerkkinä Puppet-Finland-projektin ::snmp::packetfilter -luokka, jossa avataan pääsy UDP-portiin 161 määritetyistä IPv4 ja IPv6-osoitteista.

Myös Windowsin palomuurisääntöjä voi muokata Puppetilla käyttämällä puppet/windows_firewall -moduulia. Kyseinen moduuli on melko yksinkertainen kääre "netsh advfirewall"-komennon ympärillä, mutta se hoitaa hommansa hyvin.

Linuxissa koko palomuurisäännöstö on helppo pitää ruodussa Puppetilla (ks. esim. puppetfinland/packetfilter moduuli), mutta Windowsissa on pitkälti tyytyminen sääntöjen kevyeen lisäämiseen ja muokkaamiseen. Tämä käy ilmeiseksi, kun listaa tuoreen Windows-koneen palomuurisäännöt:

PS> netsh advfirewall firewall show rule name="all"|more

Sääntöjä on valtava määrä ja niitä ovat luoneet ties mitkä sovellukset ja automatisoidut järjestelmät. Yllä oleva komento putkitetaan "more"-komennolle, jotta sääntöjä ehtisi ylipäätään lukea. Joka tapauksessa sääntöjen täydellinen hallinta Puppetilla olisi vähintäänkin haastavaa jos minkään ei haluttaisi rikkoutuvan. Tämä ei kuitenkaan mitenkään estä yksittäisten lisäsääntöjen luontia Puppetilla - alla hieman muokattu esimerkki puppetfinland/windows_snmp -moduulin palomuurisäännöistä:

 ::windows_firewall::exception { 'windows_snmp':
 ensure => 'present',
 direction => 'in',
 action => 'Allow',
 enabled => 'yes',
 protocol => 'UDP',
 local_port => '161',
 remote_ip => '10.50.10.15',
 display_name => 'SNMP-in from 10.50.10.15',
 description => "Allow SNMP connections",
 }

Display-name-parametrin oletusarvona on resurssin nimi ($title) ja siihen kannattaa lisätä säännöstä niitä parametreja, joiden voidaan olettaa muuttuvan, esimerkiksi remote_ip. Tämä siksi, että Puppet tajuaisi luoda uuden säännön, jos kyseinen parametri muuttuu.

Aivan kuten Linuxissakin pitää Puppetin luomia sääntöjä aika ajoin tarkistaa tai debugata. Puppetin luoman säännön saa esille antamalla name-parametriksi ::windows_firewall-resurssin parametrin display_name arvon:

PS> netsh advfirewall firewall show rule name="SNMP-in from 10.50.10.15"
 
 Rule Name: SNMP-in from 10.50.10.15
 ----------------------------------------------------------------------
 Enabled: Yes
 Direction: In
 Profiles: Domain,Private,Public
 Grouping:
 LocalIP: Any
 RemoteIP: 10.50.10.15/32
 Protocol: UDP
 LocalPort: 161
 RemotePort: Any
 Edge traversal: No
 Action: Allow
 Ok.

Mikäli Puppetin luomassa säännössä on jotakin vikaa, voidaan se poistaa helposti:

PS> netsh advfirewall firewall delete rule name="SNMP-in from 10.50.10.15"

Puppetissa on suuri määrä erilaisia rakenteita, joilla turhaa toistoa koodissa voidaan vähentää. Aiemmin käsiteltiin jo resurssien oletusarvoja ja each-funktiota, joilla toistoa voidaan vähentää yksittäisessä manifest-tiedostossa. Funktiolla samalle koodilohkolle voidaan välittää parametreja, joiden perusteella se laskee mekaanisesti jonkin paluuarvon. Kuten Puppetin virallisten funktioiden listasta näkyy, tätä funktioiden ominaisuutta hyödynnetään mitä erilaisimpiin käyttötarkoituksiin. Funktioita voi paitsi käyttää eri manifesteissa, myös eri moduuleissa kuin missä ne on määritetty.

Perinteisesti funktioita kirjoitettiin Ruby-kielellä. Sittemmin on tullut mahdolliseksi kirjoittaa niitä myös Puppet-kielellä. Alla yksinkertainen esimerkki puppetfinland/openvpn-moduulista:

function openvpn::baseurl(Optional[String] $baseurl) >> String
 {
 if $baseurl {
 $baseurl
 } else {
 $proto = 'puppet'
 $share = 'files'
 "${proto}:///${share}"
 }
 }

Funktion määrittelyyn kuuluu funktion nimi ("openvpn::baseurl"), sen parametrit ($baseurl) datatyyppeineen ja paluuarvon tyyppi ("String"). Funktion paluuarvoksi tulee funktion kutsussa viimeisenä käsitelty lauseke. Yllä oleva funktio tarkistaa, onko $baseurl -parametrilla arvoa ja jos on, se palauttaa sen sellaisenaan ($baseurl), ja jos ei ole, se palauttaa oletusarvon "puppet:///files". Funktion kirjoittaminen oli tässä tapauksessa järkevää, koska samaa koodilohkoa kutsutaan useissa eri paikoissa saman moduulin sisällä.

Funktion paluuarvo voidaan sijoittaa muuttujaan tai käyttää sellaisenaan. Esimerkiksi yllä olevaa funktiota käytetään tähän tapaan:

$l_files_baseurl = openvpn::baseurl($files_baseurl)
 
 file { "openvpn-${ta}":
 name => $ta_path,
 source => "${l_files_baseurl}/openvpn-${ta}",
 mode => '0600',
 }

Puppet-kielellä kirjoitetut funktiot sijoitetaan moduulin juurihakemiston alla olevaan kansioon functions. Aivan kuten luokkienkin tapauksessa niiden nimen ja tiedostonimen pitää täsmätä. Yllä oleva funktio sijaitsee siis tiedostossa functions/baseurl.pp.

Vaikka Puppet-kielellä luodut funktiot ovatkin käytetävissä ne määrittelevän moduulin ulkopuolella, on niiden polussa aina moduulin nimi (yllä siis openvpn::baseurl). Mikäli halutaan luoda funktio päätasolle ilman moduulin nimeä, on se kirjoitettava Rubyllä.

Puppet Forge on Puppetlabsin tarjoama repo Puppet-moduuleille. Forge-moduuleja voi asentaa "puppet module install"-komennolla, mutta niihin voi viitata myös r10k:n tai librarian-puppetin Puppetfilessä. Nykyisin Puppet Forge on ensimmäinen paikka, josta katson, onko joku muu jo ratkaissut käsilläni olevan ongelman Puppetilla - pyörää kun on toisinaan turha keksiä uudelleen.

Olen itse käyttänyt vuosia Git Submoduleja, koska niiden avulla Puppet-moduuleihin ulkopuolelta tulevat muutokset on helppo tarkistaa Gitin lokeista ja diffeistä. Niiden käyttö menee kuitenkin kankeaksi kun moduulien määrä kasvaa suureksi: sekä päärepo että submodulet vaativat jatkuvaa paapomista. Tuloksena on turhanpäiväisiä, samankaltaisina toistuvia commit-viestejä kuten "Update Git submodule pointers", joilla ei ole mitään muuta käytännön merkitystä kuin Gitin pitäminen tyytyväisenä.

Moduulien säilömisestä Forgessa on eräs etu käytettäessä Puppetfileä: librarian-puppet osaa ladata moduulien metadata.json:issa määritetyt riippuvuudet automaattisesti, mutta vain jos riippuvuudet löytyvät Forgesta. Tai kääntäen: Gitissä olevia riippuvuuksia ei ladata automaattisesti, vaan ne pitää määritellä yrityksen ja erehdyksen kautta käsin. Tämä paisuttaa Puppetfileä huomattavasti ja tekee sen ylläpidosta työlästä. Kyseessä on Puppetin oma version riippuvuushelvetistä. Valitettavasti r10k ei osaa ladata riippuvuuksia automaattisesti - mitään selkeää syytä tähän ei ole.

Moduulien saaminen Puppet Forgeen ei ole kovin vaikeaa. Ensin luodaan käyttäjätili Puppet Forgeen. Käyttäjätunnuksen ja moduulin metadata.json:in "name"-kohdan tulisi täsmätä. Esimerkiksi jos metadata.json sisältää

"name": "puppetfinland-os",

niin Puppet Forge-käyttäjätunnuksen tulisi olla "puppetfinland". Ilmeisesti tämä ei ole ehdoton vaatimus, mutta sen noudattaminen lienee selkeyden takia järkevää.

Forgeen voi viedä moduuleja käsin, mutta se on erittäin työlästä kun moduuleja on paljon. Seuraava askel onkin puppet-blacksmithin asennus. Tässä vaiheessa haasteeksi tulee se, mihin Gem-polkuun puppet-blacksmith asennetaan. Itse suosin asentamista /opt/puppetlabs/puppet -hakemiston alle:

$ /opt/puppetlabs/puppet/bin/gem install puppet-blacksmith

Sitten luodaan Blacksmithin konfiguraatiotiedosto, ~/.puppetforge.yml, johon laitetaan Puppet Forgen käyttäjätunnus ja salasana tähän tapaan:

---
 url: https://forgeapi.puppetlabs.com
 username: myuser
 password: mypassword
 

Tämän jälkeen voidaanin siirtyä jo itse moduuliin muokkaamiseen paremmin Forgen kanssa yhteensopivaksi. Tämä kannattaa tehdä Puppetin moduulikehitystyökaluilla eli Puppet PDK:lla, joka asennetaan näitä ohjeita seuraamalla. Vaikka virallista Debian GNU/Linux -tukea ei olekaan, PDK:n Ubuntu-versio asentuu myös Debianiin. Olemassa olevaan moduuliin saadaan PDK:lla lisättyä tukku "best practices"-läskiä helposti:

$ cd <moduulin-hakemisto>
 $ pdk convert

PDK:n muunnoskomento lisää moduuliin mm. rspec-testejä sekä Travis CI- ja Appveyor -integraatioita

Seuraavaksi PDK:n luomaan Rakefileen lisätään riippuvuus puppet-blacksmithin:

require 'puppet_blacksmith/rake_tasks'

Tämän jälkeen rakella (Rubyn "make") voidaan ajaa puppet-blacksmithin komentoja, jotka on hyvin esitelty sen virallisesta dokumentaatiosta.

Mikäli moduuliin tehdyt muutokset halutaan testata automaattisesti Travis CI:llä (mikä on ehdottoman järkevää), pitää Gemfileen lisäksi tehdä pieni lisäys:

group :development do
 --- snip ---
 gem "puppet-blacksmith"
 end

Ilman tätä muutosta Travis-ajot epäonnistuvat ("Build failed"), koska Rakefileen yllä määriteltyä puppet-blacksmith riippuvuutta ei löydy:

LoadError: cannot load such file -- puppet_blacksmith/rake_tasks

Traviksen kanssa kannattaa myös varmistua siitä, että metadata.jsonissa määritelty ohjelmistolisenssin nimi vastaa sen virallista tunnistetta ("Identifier"), jonka voi etsiä esimerkiksi täältä. Muutoin Traviksen ajama metadata_lint -testi epäonnistuu:

(WARN) license: License identifier BSD license is not in the SPDX list: http://spdx.org/licenses/
 Warnings found in metadata.json
 
 The command "bundle exec rake $CHECK" exited with 1.

Tässä vaiheessa kaikki Git-repossa olevat muutokset kannattaa kommitoida.

Nyt puppet-blacksmithin pitäisi olla jo toimintakykyinen. On kuitenkin järkevää ensin tarkistaa, että Traviksen ajamat testit menevät läpi pushaamalla muutokset Gitiin:

$ git push

Jos testit näyttävät vihreää, voidaan moduulista julkaista uusi versio. Teoriassa komennon "rake module:release" pitäisi hoitaa koko julkaisuprosessi, mutta siinä olevan bugin takia prosessi on valitettavasti vähemmän suoraviivainen. Ensin muutetaan moduulin versio metadata.jsonissa ja kommitoidaan muutokset:

$ rake module:bump_commit:patch
 Bumping version from 0.1.5 to 0.1.6

Sitten luodaan metadata.jsonin versiota vastaava Git tag ja pushataan se:

$ rake module:tag
 $ git push --tags

Lopuksi julkaistaan moduuli Puppet Forgessa:

$ rake module:clean
 $ rake module:push
 Uploading to Puppet Forge puppetfinland/systemd

Komento "module:clean" varmistaa, että Forgeen ladataan uusin versio.

Joskus käy niin että verkosta löytyy huolellisesti vuosien mittaan käsin säädetty, mahdollisesti jo edesmenneeltä sysadminilta peritty taideteos, jonka sielunelämästä ei kukaan enää ymmärrä, ja joka uhkaa romahtaa jos hengittää kohti liian kovaa. Se tuottaa mahdollisesti vielä jotakin yritykselle merkityksellistä palveluakin. Mikä avuksi?

Jos näin on päässyt käymään, tai odotetaan suoritettavan muun toistaiseksi villinä ja vapaana ajelevan järjestelmän hallinta puppetilla, on hyvä palauttaa mieleen, että puppet sisältää RAL-kuoren (Resource Abstraction Layer), jonka avulla voidaan tarkastella sitä miten puppet näkee järjestelmän. RAL-kuoren avulla voidaan järjestelmän tila muuntaa puppet-koodiksi pienellä vaivalla. Esimerkiksi hakemistopuun muuntaminen:

$ find `pwd` | while read file; do puppet resource file $file; done
 file { '/tmp/tmp':
 ensure => 'directory',
 ctime => '2018-03-28 14:58:55 +0300',
 group => '0',
 mode => '0755',
 mtime => '2018-03-28 14:58:55 +0300',
 owner => '0',
 selrange => 's0',
 selrole => 'object_r',
 seltype => 'user_tmp_t',
 seluser => 'unconfined_u',
 type => 'directory',
 }
 file { '/tmp/tmp/tmp':
 ensure => 'directory',
 ctime => '2018-03-28 14:59:14 +0300',
 group => '0',
 mode => '0755',
 mtime => '2018-03-28 14:59:14 +0300',
 owner => '0',
 selrange => 's0',
 selrole => 'object_r',
 seltype => 'user_tmp_t',
 seluser => 'unconfined_u',
 type => 'directory',
 }
 file { '/tmp/tmp/tmp/file':
 ensure => 'file',
 content => '{md5}d41d8cd98f00b204e9800998ecf8427e',
 ctime => '2018-03-28 14:59:43 +0300',
 group => '0',
 mode => '0640',
 mtime => '2018-03-28 14:59:14 +0300',
 owner => '0',
 selrange => 's0',
 selrole => 'object_r',
 seltype => 'user_tmp_t',
 seluser => 'unconfined_u',
 type => 'file',
 }

File-resurssi tarvitsee syötteeksi absoluuttisen polun, joten myös find-komennolle annetaan se. Tämä varmistaa että find myös tulostaa em. putkitukselle absoluuttisen polun.

Ainoa miinus File-resurssien kanssa on se, että owner ja group näkyvät niissä numeerisina. Onkin järkevintä muuntaa numeerisen arvot nimiksi esim. id-komennolla:

$ id -un 0
 root
 $ id -gn 0
 root

Ensimmäinen komento palauttaa käyttäjän 0 (UID=0) nimen ja jälkimmäinen ryhmän 0 (GID=0) nimen.

Puppet-koodissa on monia tapoja vähentää koodiin määrää. Silloin, kun pitää luoda useita hyvin samankaltaisia resursseja, on each-funktio erittäin kätevä työkalu. Jos luotavien resurssien erot rajoittuvat yhden parametrin arvoon, riittää seuraava:

# testfiles.pp
 $testfiles = ['/tmp/foo','/tmp/bar']
 $testfiles.each |$testfile| {
 file { $testfile:
 ensure => 'present',
 }
 }

Yllä oleva koodi luo molemmat tiedostot:

$ puppet apply testfiles.pp
 Notice: Compiled catalog for computer.example.org in environment production in 0.13 seconds
 Notice: /Stage[main]/Main/File[/tmp/foo]/ensure: created
 Notice: /Stage[main]/Main/File[/tmp/bar]/ensure: created
 Notice: Applied catalog in 0.03 seconds

Each-funktio on erityisen voimallinen, kun sen käyttämä data määritetään Hash-tyyppisessä muuttujassa. Alla esimerkki ini-tiedoston muokkauksesta gitdaemon-moduulissa:

$ini_settings = { 'GIT_DAEMON_ENABLE' => true,
 'GIT_DAEMON_BASE_PATH' => $base_path,
 'GIT_DAEMON_DIRECTORY' => $directory, }
 
 $ini_settings.each |$item| {
 ini_setting { $item[0]:
 ensure => 'present',
 path => '/etc/default/git-daemon',
 setting => $item[0],
 value => $item[1],
 }
 }

Toinen koodia säästävä lähestymistapa on käyttää resurssien oletusarvoja:

Ini_setting {
 ensure => 'present',
 path => '/etc/default/git-daemon',
 }
 
 ini_setting { 'GIT_DAEMON_ENABLE':
 setting => 'GIT_DAEMON_ENABLE',
 value => true,
 }
 
 ini_setting { 'GIT_DAEMON_BASE_PATH':
 setting => 'GIT_DAEMON_BASE_PATH',
 value => $base_path,
 }
 
 ini_setting { 'GIT_DAEMON_DIRECTORY':
 setting => 'GIT_DAEMON_DIRECTORY',
 value => $directory,
 }

Resurssien oletusarvojen kanssa pitää kuitenkin olla tarkkana, sillä niiden vaikutusalue voi laajentua puolivahingossa. Käytännössä vaikuttaa siltä, että niitä voi turvallisesti käyttää tiedostoissa, joista ei kutsuta muita luokkia tai määriteltyjä resursseja, joihin oletusarvot voisivat "valua". Puppetista löytyy myös turvallisempi tapa toteuttaa suunnilleen sama toiminnallisuus.

Yllä olevan kolmen ini-asetuksen määrittely suoraan olisi vaatinut 18 koodiriviä ja lopputulos olisi sisältänyt runsaasti toistoa. Resurssien oletusarvoilla päästiin 16 koodiriviin. Each-funktiota käyttäen päästiin 11 koodiriviin. Suuremmalla resurssien määrällä säästö olisi ollut vielä huomattavampi, koska jokainen uusi resurssi olisi lisännyt vain yhden rivin koodia.

Staattisten faktojen kirjoittaminen on varsin helppoa melko vähäisinkin Ruby-taidoin. Alla esimerkki faktasta, joka palauttaa true tai false riippuen siitä, onko noodilla /boot-osiota:

Facter.add(:has_bootfs) do
 setcode do
 if Facter.value(:mountpoints)['/boot'].nil?
 false
 else
 true
 end
 end
 end

Kuten yltä näkyy, itse tieto löytyi jo valmiiksi Facterin "mountpoints"-faktan sisältä: yllä sen sisältöä vain muunnettiin erilliseksi, helposti käsiteltäväksi faktaksi visualisointia ja skriptausta helpottamaan.

Mountpoints-fakta sisältää myös paljon muuta kiinnostaa tietoa, kuten sen, miten suuri osio on ja paljonko siellä on tilaa vapaana. Jos näistäkin tiedoista halutaan luoda samaan tapaan erilliset faktat, voidaan ne helposti luoda dynaamisesti:

if Facter.value(:has_bootfs)
 facts = [ 'size','available']
 
 facts.each do |fact|
 Facter.add("bootfs_#{fact}") do
 setcode do
 Facter.value(:mountpoints)['/boot'][fact]
 end
 end
 end
 end

Yllä oleva koodi siis luo kaksi uutta faktaa mutta vain jos /boot-osio on olemassa:

Näitä faktoja voidaan käyttää normaaliin tapaan:

$ facter -p has_bootfs
 true
 $ facter -p bootfs_size
 235.32 MiB
 $ facter -p bootfs_available
 111.28 MiB

Vaikka yllä olevassa esimerkissä ei varsinaisesti luoda mitään uutta dataa, voi samalla strategialla muodostaa räätälöityjä faktoja dynaamisesta sisällöstä. Alla luodaan erillinen fakta ("user_<username>_is_present") jokaisesta *NIX-järjestelmässä olevasta käyttäjästä pois lukien järjestelmäkäyttäjät:

require 'etc'
 
 Etc.passwd do |entry|
 # Normal users have IDs in this range in /etc/login.defs
 if entry.uid >= 1000 and entry.uid <= 60000
 Facter.add("user_#{entry.name}_is_present") do
 setcode do
 true
 end
 end
 end
 end

Näiden dynaamisesti luotujen faktojen käyttö on helppoa:

$ facter -p user_john_is_present
 true
 $ facter -p user_jack_is_present
 
 $ facter -p user_jane_is_present
 true

Ainoa rajoitus on se, että datasta, jota ei ole olemassa, ei voida tietenkään luoda faktaa. Esimerkiksi yllä fakta käyttäjästä "jack", jota ei ole olemassa, saa tyhjän arvon (undef/nil) sen sijaan, että se palauttaisi totuusarvon false. Tällä ei Puppet-koodin tai faktojen visualisoinnin kannalta ole kuitenkaan suurta merkitystä.

NOTE: this article is now also available in English (here).

Aiemmassa blogikirjoituksessa käsittelin Augeaksen käyttöä Puppetin kanssa esimerkkinä PostgreSQL:n pg_hba.conf -tiedoston muokkaus. Tässä kirjoituksessa käsitellään XML-tiedostojen muokkausta Augeaksella, mikä on luultavasti siistein ratkaisu silloin, kun ei haluta hallita koko tiedostoa esimerkiksi ERB-templatena. Alla muokataan Pwm:n web.xml-tiedostoa, johon on määritettävä asetushakemiston polku.

Ennen itse Augeas-resurssin kirjoitusta kannattaa tarkistaa, mikä Augeaksen käsitys muokattavan tiedoston rakenteesta on. Ensin augtoolille pitää kertoa, mistä hallittava tiedosto löytyy:

$ augtool
 augtool> set /augeas/load/Xml/incl[1] /var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml
 augtool> load

Nyt Augeas näkee var-hakemiston polussa /files:

augtool> ls /files
 etc/ = (none)
 lib/ = (none)
 home/ = (none)
 boot/ = (none)
 augtool> load
 augtool> ls /files
 etc/ = (none)
 lib/ = (none)
 home/ = (none)
 boot/ = (none)
 var/ = (none)

Varmistetaan vielä, että muokattava tiedosto näkyy Augeakselle:

augtool> ls var/lib/tomcat8/webapps/pwm/WEB-INF/
 web.xml/ = (none)

Nyt katsotaan, miltä tiedosto Augeaksen mielestä näyttää. Tämä komento tuottaa valtavan määrän tulostetta, josta alla näytetään vain oleellinen:

augtool> print var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/
 --- snip ---
 /files/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/web-app/context-param/param-name
 /files/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/web-app/context-param/param-name/#text = "applicationPath"
 /files/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/web-app/context-param/#text[3] = " "
 /files/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/web-app/context-param/param-value
 /files/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/web-app/context-param/param-value/#text = "unspecified"
 /files/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml/web-app/context-param/#text[4] = " "
 --- snip --

Tämän pohjalta voidaan jo luoda varsinainen Augeas-resurssi:

 augeas { 'pwm applicationPath':
 incl => "/var/lib/tomcat8/webapps/pwm/WEB-INF/web.xml",
 lens => 'Xml.lns',
 changes => "set web-app/context-param/param-value/#text /etc/pwm",
 }

Resurssi kannattaa tallentaa esim. augtest.pp -tiedostoon ja alustavasti testata puppet apply-komennolla:

$ puppet apply augtest.pp

Tämän jälkeen koodin voikin jo integroida haluttuun Puppet-moduuliin (tässä tapauksessa puppet-pwm).

Puppetin palvelinpuoleen kuuluu on nykyisin aikamoinen tukku palvelinsovelluksia, työkaluja ja suositeltuja työnkulkuja:

  1. Palvelinkomponentit
    • Puppetserver: Varsinainen Puppet-palvelin, johon Puppet agentit ottavat yhteyttä. Toimii myös itsenäisesti ilman PuppetDB:tä ja Puppetboardia
    • PuppetDB: Datasäilö, johon Puppet agentit tallentavat faktojaan ja raporttejaan. Dataan päästään haluttaessa käsin ohjelmallisesti.
    • PostgreSQL: PuppetDB:n taustalla oleva tietokantapalvelin
    • Puppetboard: PuppetDB:n datan visualisoitiin tarkoitettu webbisovellus.
    • Apache 2: Puppetboardin taustalla oleva webbipalvelin
  2. Työkalut
    • Git: versionhallintajärjestelmä, josta hallintarepo, r10k ja monien Puppet moduulien käyttö on riippuvaista.
    • Hiera: hierarkinen datasäilö Puppet-moduulien, esim. profiilien, parametreille. Voidaan käyttää asiakaskoneiden asetusten räätälöintiin eri luokitteluperustein. Luokittelu voi perustua faktoihin (esim. käyttöjärjestelmäversio, datakeskus) tai asiakaskoneelle erikseen määritettyihin arvoihin (esim. asiakas, koneen kriittisyys liiketoiminnan kannalta).
    • Eyaml: Hieran datatiedostossa (yaml) olevan datan kryptaukseen tarkoitettu työkalu. Tärkeä erityisesti silloin, kun Hieran dataa säilötään organisaation ulkopuolella kuten esimerkiksi GitLabissa.
    • R10k: työkalu dynaamisten Puppet environmentien hallintaan. Käytännössä r10k luo kustakin hallintarepon haarasta Puppet environmentin. Kukin haara sisältää environmentin vaatimat Puppet-moduulit sekä niiden tarvitsemat parametrit.
  3. Suositellut työnkulut

Puppet-moduulien ja manifestien uudelleenkäytettävyyden vuoksi on suositeltavaa pitää data niistä erillään. Tämä tapahtuu käyttämällä hieraa. Moduulit voivat esimerkiksi tuottaa erilaisia asetuksia käyttämällä hieran perusteella organisaatiohin tai sijainteihin määriteltyä dataa

Hieraan tallennetaan tyypillisesti myös sellaista yksityistä dataa mikä pitää olla suojattua, ja minkä ei haluta joutuvan ulkopuolisten saataville. Tyypillisesti data tallennetaan muun koodin ohella versionhallintajärjestelmään, joka voi olla organisaation rajojen ulkopuolella. Tällöin on käytettävä hieran encryptionta.

Aiemmin yleisesti käytetty hieran salauksen menetelmä oli hiera-gpg. Hiera-gpg:n avulla salattiin koko tiedosto. Hiera-eyaml parantaa tilannetta sallimalla hieratiedoston sisällä haluttujen datalohkojen salaamisen, mikä parantaa käytettävyyttä.

Hiera-yaml pitää ensin asentaa:

$ sudo gem install hiera-eyaml
Puppet-palvelimella asennus tapahtuu seuraavasti:
$ sudo puppetserver gem install hiera-eyaml

Hiera-eyaml käyttää asymmetristä enkryptointia, oletuksena PKCS#7:ää. Sen käyttöön tarvitaan siis avainpari. Julkisella avaimella voidaan purkaa salattujen datalohkojen encryption ja yksityisellä avaimella salata. Avainpari luodaan komennolla:

$ eyaml createkeys

Avainpari luodaan sen hetkisen hakemiston keys-alihakemistoon. Luodut avaimet tulee suojata ja säilyttää huolellisesti. Salauksen tuottamiseen riittää että julkinen avain on käytettävissä. Salatun lohkon muokkaamiseen tarvitaan sekä julkinen että yksityinen avain. Henkilö, joka lisää tai muokkaa hiera-tiedostojen sisältöä tarvitsee siis molemmat avaimet. Avaimet tulee suojata vastaavasti kuten palvelimellakin, eli omistajaksi määritetään käyttäjä ja tiedostoille vain lukuoikeus käyttäjälle itselleen. Sopiva sijainti on esimerkiksi kotihakemiston keys-alihakemisto.

Puppetserver tarvitsee salauksen purkamiseen ja arvojen lukemiseen sekä julkisen että yksityisen avaimen. Puppetserverillä sopiva sijainti avaimille on esim.:

/etc/puppetlabs/puppet/eyaml
Luodaan siis tämä hakemisto ja säädetään sen oikeudet minimiin.
$ mkdir /etc/puppetlabs/puppet/eyaml
 $ chown -R puppet:puppet /etc/puppetlabs/puppet/eyaml
 $ chmod -R 0500 /etc/puppetlabs/puppet/eyaml
 $ chmod 0400 /etc/puppetlabs/puppet/eyaml/*.pem
Tarkistetaan että tiedostojen omistajat ja oikeudet ovat halutunlaiset:
$ ls -lha /etc/puppetlabs/puppet/eyaml

Omistajana tulee siis olla puppet, ryhmäomistajana puppet, ja lukuoikeus vain puppet-tunnukselle.

Jos käyttäjä haluaa muokata omalle koneellaan olemassaolevan hiera-tiedoston arvoja, tyypillisesti hallintarepon kopiossa, tarvitaan molemmat avaimet. Avaimet tulee siirtää omalle koneelle asianmukaisella turvallisella protokollalla, esim. scp:llä. Muokkaamisen helpottamiseksi on hyödyllistä luoda kotihakemistoon uusi hakemisto .eyaml, ja sinne konfigurointitiedosto config.yaml, jossa määritetään mistä avaimet löytyvät:

---
 pkscs7_public_key: '/Users/tunnus/keys/public_key.pkcs7.pem'
 pkcs7_private_key: '/Users/tunnus/keys/private_key.pkcs7.pem'

Kun avaimet ovat palvelimella paikoillaan, voidaan määrittää hieran konfigurointitiedosto käyttämään näitä avaimia. Uudessa hiera-versiossa 5 on itsenäiset hierarkian määritykset jokaiselle environmentille ja moduulille. Hiera.yaml voidaan siis sijoittaa hallintarepoon ja sen haaroihin, ja viitata näissä edellä luotuihin avaimiin palvelimella. Oletetaan että yksityiselle avaimelle on annettu nimi private_key.pkcs7.pem ja julkiselle avaimelle public_key.pkcs7.pem.

---
 version: 5
 defaults:
 datadir: data
 data_hash: yaml_data
 
 hierarchy:
 - name: "Secret data"
 lookup_key: eyaml_lookup_key
 paths:
 - "secrets.eyaml"
 options:
 pkcs7_private_key: /etc/puppetlabs/puppet/eyaml/private_key.pkcs7.pem
 pkcs7_public_key: /etc/puppetlabs/puppet/eyaml/public_key.pkcs7.pem
 
 - name: "Per-node data"
 path: ”nodes/%{trusted.certname}.yaml"
 - name: "Common data"
 path: "common.yaml"
*nix-tyyppisissä käyttöjärjestelmissä voidaan luoda ympäristömuuttuja EDITOR joka määrittää käyttäjän haluaman oletusmuokkaimen. Ympäristömuuttuja voidaan laittaa esim. bash-käyttäjän kuoren alustustiedostoon ~/.bashrc. Tai se voidaan antaa suoraan komentorivillä:
$ export EDITOR=emacs
Kun ympäristömuuttuja on olemassa, voidaan hierassa määritettyä eyaml-muotoisen tiedoston datalohkoja muokata seuraavasti
$ eyaml edit secrets.eyaml

Eyaml avaa tällöin tiedoston ohjeen kera. Salattavaksi haluttu arvo laitetaan tällöin hakasulkeiden väliin muotoon DEC::PKCS7[]!

#| This is eyaml edit mode. This text (lines starting with #| at the top of the
 #| file) will be removed when you save and exit.
 #| - To edit encrypted values, change the content of the DEC(<num>)::PKCS7[]!
 #| block.
 #| WARNING: DO NOT change the number in the parentheses.
 #| - To add a new encrypted value copy and paste a new block from the
 #| appropriate example below. Note that:
 #| * the text to encrypt goes in the square brackets
 #| * ensure you include the exclamation mark when you copy and paste
 #| * you must not include a number when adding a new block
 #| e.g. DEC::PKCS7[]!
 ---
 # This value will not be encrypted
 plaintextvalue: plaintext
 # This value will be encrypted
 encryptedvalue: DEC::PKCS7[encrypted]!

Jos nyt katsotaan tiedostoa, voidaan todentaa että edellä määritetty arvo on salattu:

$ cat secrets.eyaml
 ---
 # This value will not be encrypted
 plaintextvalue: plaintext
 # This value will be encrypted
 encryptedvalue: ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAK/YGRbR2qbgpxHxrCic6ywXG6x0w0hZksNQqJPBYTq2FDyDO7H9L0XlVmnSP+wpjEleDGBJqUEyxgucYICvub5QaHQukBJ7/5ZeQ3grGIBOQkvEZVONWjNtdA+MkiIrc/erasgWYaU8lVJZ73RC6VzJQHYdphCsxue10kTAQw1uBKZOCbc9qHlhIwJuNERfUZBsfMpWgmnExph3kBsVlQ4FPTurkX2Kp0wEQlDVKm5llv4juq3dQLhDS4NkxmdopX/8jWP8+TMQB7vfW5kgS2U08vlm9QKgukO6GMeDrn/1Y66KnbokfGh4eJF7L94A1EYpKQx5eja+ITkvGarvCSTA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBBn5gfCNVVsnRmQgIhE23pZgBAHsuh7FqP+XQMbJCQlREMN]

Linkkejä:

Artikkelisarjan muut osat:

Mikäli hallintarepon, r10k:n ja GitLab:in sisältävä työnkulku tuntuu vielä liian kevyeltä, on läskiä ja abstraktiotasoja lisättävissä onneksi toisaalla, nimittäin itse Puppet-moduuleissa. Pelastuksen tarjoaa ns. "Roles and Profiles pattern", jonka kehitti alunperin Craig Dunn, ja joka löi läpi todennäköisesti Gary Larizzan blogikirjoitusten myötä. Alla käytämme englanninkielisen termin sijaan lähes suomenkielistä termiä "roolit ja profiilit".

Roolit ja profiilit ovat aivan tavallisia Puppet-luokkia. Ne eivät ole kuitenkaan ole Gary Larizzan termiä käyttäen ns. komponenttimoduuleja, jotka on suunniteltu jonkin yksittäisen asian tekoon, ja jotka olisivat siten (ainakin periaatteessa) jaettavissa muiden käyttäjien kesken esim. Puppet Forgessa.

Profiilien tarkoitus on kasata suurehko kasa toiminnallisuutta, esimerkiksi jokin "Technology Stack" yhteen. Hyvä esimerkki on vaikkapa jokin verkkosovellus, jonka asennus vaatii verkkopalvelimen (nginx, apache), tietokantapalvelimen (postgresql, mysql) ja monen muun asian laittamista kuntoon. Tällaisen sovelluksen konfiguraatiot voisi toki toteuttaa myös "tavallisella" Puppet-moduulilla, mutta moduulin uudelleenkäytettävyys olisi varsin rajattua kahdesta syystä:

Tässä tulevat apuun profiilit, joita ei ole tarkoitettukaan yleiskäyttöisiksi, vaan lähinnä korvaamaan luokkien ja niiden kaikkien parametrien määrittely suoraan Hierassa. Toisin sanoen profiililla kasataan komponenttimoduuleista toimivia kokonaisuuksia. Parametrit poimitaan profiili-luokassa suoraan Hierasta lookup-funktiolla.

Alla esimerkki yksinkertaisesta profiilista, jolla konfiguroidaan tyypillisen *NIX-palvelimen perusasetukset:

#
 # == Class: profile::unixbase
 #
 # *NIX-tyyppisten koneiden yleiset asetukset eräässä ympäristössä
 #
 class profile::unixbase {
 
 # Oletussähköpostiosoite, jota monet moduulit käyttävät
 $email = lookup('default_email')
 
 # Sisäverkon osoite ja verkkomaski, esimerkiksi muotoa
 # 10.95.5.0/24
 $intranet = lookup('intranet')
 
 # Puppet-palvelimen osoite
 $puppet_server = lookup('puppet_server')
 
 # Luokat ja määritetyt resurssit
 include ::bash
 bash::config::user { 'samuli':
 ensure => 'present',
 }
 include ::localbackups
 class { '::locales':
 locales => [ 'en_US.UTF-8 UTF-8',
 'fi_FI.UTF-8 UTF-8',
 'it_IT.UTF-8 UTF-8', ]
 }
 include ::localusers
 class { '::mdns':
 allow_ipv4_address => $intranet,
 }
 include ::nano
 class { '::ntp':
 ensure => 'running',
 ntp_pools => [ '0.fi.pool.ntp.org',
 '1.fi.pool.ntp.org',
 '2.fi.pool.ntp.org', ]
 }
 include ::postfix
 class { '::puppetagent':
 master => $puppet_server,
 }
 class { '::puppetagent::cron':
 splaylimit => '10m',
 email => $email,
 }
 include ::ssh
 class { '::sshd':
 passwordauthentication => 'yes',
 permitrootlogin => 'without-password',
 }
 include ::systemd
 class { '::timezone':
 timezone => 'Europe/Helsinki',
 }
 class { '::updater':
 hour => 13,
 minute => 5,
 weekday => '*',
 install => 'yes',
 mailon => 'error',
 }
 }

Yllä olevasta profiilista näkee helposti yleisellä tasolla mihin järjestelmän konfiguraatioihin se koskee. Ylhäällä haetaan muutama "globaali" parametri Hierasta ja käytetään niitä luokkien parametreina. Lookup-funktio kutsut tehdään heti profiilin alussa, jotta profiilin käyttämät parametrit näkyvät helposti yhdestä paikasta sen sijaan, että ne olisi ripoteltu pitkin profiilia.

Parametrien hausta lookup-kutsulla on hyötyä tietyn tyyppisten arvojen kanssa:

Muissa tapauksissa parametrien arvot voi turvallisesti kirjoittaa suoraan profiilin luokkakutsuihin, jolloin pelkästään profiilia katsomalla saa varsin hyvän käsityksen mitä se tekee.

Se, miksi profiileja ei tehdä parametrisoiduiksi luokiksi vaan käytetään "maagisia" Hiera-lookuppeja ei ole ainakaan minulle täysin selvää: täytyyhän profiilin tarvitsemat parametrit joka tapauksessa lisätä Hieraan. Muutama etu ei-parametrisoiduista profiileista kuitenkin on:

Roolit ovat vielä korkeamman tason abstraktiotasolla ja niissä kasataan yhteen ainoastaan profiileja:

#
 # == Class: role::webserver
 #
 class role::webserver {
 include ::profile::unixbase
 include ::profile::nginx
 }

Noodin yaml-tiedoston tiedostoon tarvitsee tämän jälkeen lisätä vain yksi rivi, jotta koneesta saadaan rakennettua webbipalvelin:

classes:
 - role::webserver

Rooleissa ja profiileissa on siis pohjimmiltaan kyse entistä suurempien, ainakin organisaation sisällä uudelleenkäytettävien Puppet-luokkien luomisesta sen sijaan, että pienistä komponenttimoduuleista kasattaisiin toimivia, suuren määrän parametreja ja luokkia sisältäviä kokonaisuuksia Hieran noodikohtaisissa yaml-tiedostoissa.

Artikkelisarjan muut osat:

Puppet on jo pitkään tukenut Windowsia. Monet natiivit Puppet-resurssit, kuten File, Package ja Service ovat jo pitkään toimineet sekä *NIX-käyttöjärjestelmissä että Windowsissa. Lisäksi osa natiiveista Puppet-resursseista, kuten Registry on suunniteltu pelkästään Windows-käyttöön.

Puppetin käyttömukavuus Windows-järjestelmien ylläpidossa on parantunut mutkan kautta muutamien uusien teknologioiden myötä:

  1. Powershell DSC
  2. Chocolatey
  3. Powershell Exec provider

Powershell DSC perustuu resurssien tilan hallintaan, aivan kuten Puppetkin. Mikä parasta, Puppet-koodissa voi käyttää natiivien Puppet-resurssien lisäksi Powershell DSC-resursseja. Puppetlabsin blogikirjoitus on hyvä alustus tähän aiheeseen. DSC vaatii toimiakseen Powershell 5.0:n, joka tulee Windows Management Framework 5.0:n mukana.

Chocolatey on pakettienhallintajärjestelmä sekä julkinen pakettivarasto Windowsille. Se perustuu Nuget-teknologiaan, jolla ohjelmistokehittäjät ovat perinteisesti asentaneet .NET-sovellusten kirjastoriippuvuuksia. Chocolatey siis laajentaa Nugetista, ohjelmistokehittäjien työkalusta, järjestelmäylläpitäjien työkalun. Käytännössä Chocolatey mahdollistaa Windows-sovelluksien asentamisen komentoriviltä samaan tapaan kuin apt-get Ubuntuissa ja Debianeissa. Hyödyllisimmillään Chocolatey on silloin, kun sitä käytetään Puppetista käsin niin sanottuna Package providerina.

Viimeisenä tulee Powershell Exec provider, jolla voidaan ajaa Powershell-skriptejä Puppetin natiivilla Exec-resurssilla.

Kaikkien kolmen käyttö Puppetissa on varsin yksinkertaista. Alla oletetaan, että Puppetserver on toimintakunnossa ja Windows-kone on liitetty siihen. Aluksi lisätään tarvittavat Puppet-moduulit Puppetserverin modules-hakemistoon:

$ git clone https://github.com/chocolatey/puppet-chocolatey chocolatey
 $ git clone https://github.com/puppetlabs/puppetlabs-dsc.git dsc
 $ git clone https://github.com/puppetlabs/puppetlabs-powershell powershell
 $ git clone https://github.com/Puppet-Finland/puppet-wmf wmf

Moduuleista viimeinen asentaa Windows Management Framework 5.0:n Chocolateyllä.

Tämän jälkeen Windows-koneen yaml-tiedostoon Hierassa lisätään luokka "wmf":

classes:
 - wmf

Seuraavan Puppet-ajon jälkeen Powershell DSC, Chocolatey Package provider ja Powershell Exec provider ovat valmiita käytettäväksi. Niiden toiminnan voi varmistaa luomalla yksinkertaisen Puppet-moduulin Puppetmasterin modules-kansioon:

$ cd <modules-dir>
 $ mkdir -p windowstest/manifests

Sitten luodaan tekstitiedosto dsctest/manifests/init.pp, jonka sisältö voi olla vaikka seuraavanlainen:

#
 class windowstest {
 
 dsc_file { 'test':
 dsc_ensure => 'present',
 dsc_destinationpath => 'C:dsc_testfile.txt',
 dsc_contents => 'foobar',
 }
 exec { 'testexec':
 provider => 'powershell',
 command => 'Add-Content -Path C:exec_testfile.txt
 -Value foobar',
 creates => 'C:exec_testfile.txt',
 }
 package { 'notepadplusplus':
 ensure => 'present',
 provider => 'chocolatey',
 }
 }

Sitten muokataan Windows-koneen yaml-tiedostoa Hierassa:

classes:
 - wmf
 - windowstest

Lopuksi Windows-koneelta voidaan tarkistaa onko Notepad++ asentunut ja onko Puppet luonut tekstitiedostot DSC:llä ja Powershell Exec providerilla:

> Test-Path 'C:Program FilesNotepad++'
 True
 > Get-Content C:dsc_testfile.txt
 foobar
 > Get-Content C:exec_testfile.txt
 foobar

Puppet on erittäin kätevä erityisesti hyvin monimutkaisten järjestelmien rakentamisessa ja ylläpidossa. Sen avulla on helppoa myös häivyttää suuriakin käyttöjärjestelmäeroja yhden luokkarajapinnan taakse. Hyvänä esimerkkinä käyvät Puppet-Finland-projektin thunderbird- ja openvpn-moduulit, jotka toimivat useilla eri Linux-jakeluilla ja Windowsilla.

Yleensä Puppettia varten rakennetaan palvelinympäristö, joka koostuu puppetserveristä, yleensä puppetdb:stä sekä mahdollisesti myös puppetboardista tai foremanista. Kullekin hallittavalle koneelle asennettu puppet-agent ottaa yhteyden puppetserveriin esim. kerran puolessa tunnissa, päivittää konfiguraationsa vastaamaan puppetserverillä määriteltyä tilaa ja lähettää raportin puppetserverin kautta puppetdb:lle.

Puppettia voidaan ajaa myös paikallisesti ilman puppetserveriä käyttämällä puppet apply-komentoa esimerkiksi seuraavasti:

$ puppet apply init.pp

Yksittäisellä manifest-tiedostolla (yllä init.pp) ei kuitenkaan pystytä tekemään ihmeitä. Puppet apply osaakin käyttää paikallisessa hakemistossa olevia puppet-moduuleita:

$ puppet apply --modulepath=/opt/puppet-modules init.pp

Yllä hakemisto /opt/puppet-modules sisältää kaikki ne moduulit, joita init.pp tarvitsee. Lisäksi siellä on oltava asennettuna moduulien vaatimat riippuvuudet. Moduulikansion ylläpito käsin on melko työlästä, mutta apuna voi käyttää esimerkiksi librarian-puppet-työkalua.

Puppet apply-pohjainen konfigurointi voidaan viedä astetta pidemmälle Kafolla. Kafolla puppet applyn päälle rakennetaan oikea asennusohjelma, jonka toimintaa voidaan ohjata komentoriviparametreilla. Kullekin Kafo-asennusohjelmalle on hyvä luoda rajapinnaksi räätälöity, yksinkertainen Puppet-moduuli muutamastakin eri syystä:

Kafo-asentimien luontiin vaadittava ympäristö on helppoa luoda Vagrant + Virtualbox -yhdistelmällä:

$ git clone https://github.com/Puppet-Finland/puppet-kafo
 $ cd puppet-kafo
 $ vagrant up kafo

Kun virtuaalikone on pystyssä, yhdistetään siihen seuraavasti:

$ vagrant ssh kafo

Sen jälkeen luodaan virtuaalikoneella hakemisto Kafo-asentimelle:

$ mkdir puppetmaster-installer
 $ cd puppetmaster-installer
 $ kafofy -n puppetmaster-installer

Asentimen hakemistoon on nyt luotu Kafon vaatima hakemistorakenne konfiguraatiotiedostoineen. Seuraavassa vaiheessa hakemistoon asennettaan asentimen vaatimat Puppet-moduulit. Tämän voisi tehdä käsinkin, mutta librarian-puppet -työkalulla homma hoituu kätevämmin:

$ rm -rf modules
 $ librarian-puppet init

Tämän jälkeen määritetään asentimen tarvitseman moduulit ja niiden version Puppetfile-tiedostossa, jonka "librarian-puppet init" loi. Alla esimerkki hyvin yksinkertaisesta Puppetfilestä, jossa kaikki moduulit on määritetty noudettaviksi Git-repositoryistä:

#!/usr/bin/env ruby 
 #^syntax detection
# Puppet Forgen osoite 
forge "https://forgeapi.puppetlabs.com"
# Kafo-installerin rajapintaluokka ("entrypoint") 
mod 'puppetfinland-puppetmaster', 
:git => 'https://github.com/Puppet-Finland/puppet-puppetmaster.git'
# Luokka, jolla asennetaan puppetserver 
mod 'puppet-puppetserver', 
:git => 'https://github.com/Puppet-Finland/puppet-puppetserver.git'
# Stdlib, jota hyvin monet muut luokat käyttävät. m
od 'puppetlabs-stdlib', 
:git => 'https://github.com/puppetlabs/puppetlabs-stdlib.git', 
:ref => '4.19.0

Puppetfileen voidaan määrittää myös moduuleja, jotka noudetaan Gitin sijaan Puppet Forgesta. Puppet Forgen käytön etuna on se, että librarian-puppet osaa moduulien vaatimat moduulit automaattisesti. Sen sijaan Gitiä käytettäessä moduulien vaatimukset on määritettävä itse Puppetfileen.

Rajapintamoduulin (yllä puppet-puppetmaster) pääluokka manifests/init.pp voi olla hyvinkin yksinkertainen:

# A simple wrapper class for setting up puppetmasters. Primarily 
# aimed at use in Kafo installers. 
# 
# == Parameters: 
# 
# $puppetserver:: Setup puppetserver 
# class puppetmaster 
( Boolean $puppetserver = true 
) 
{ if $puppetserver { 
class { 'puppetserver::repository': } 
class { 'puppetserver': } 
} 
}

Huom! Kafo on erittäin tarkka parametrien dokumentaation muodosta. Esim. parametrien dokumentaatiossa ei sallita sarkaimia. Dokumentaatio pitää siis kirjoittaa juuri yllä esitetyssä muodossa.

Kun Puppetfile on luotu, voidaan siellä määritetyt moduulit asentaa moduulikansioon librarian-puppetilla:

$ librarian-puppet install 
 $ ls modules 
 apt 
 augeas 
 augeasproviders_core 
 puppetmaster 
 puppetserver 
 puppetserver_gem 
 stdlib
 
Kuten yltä nähdään, librarian-puppet nouti Puppet Forgesta automaattisesti myös sellaisia moduuleja, joita ei oltu määritetty Puppetfileen käsin (apt, augeas, augeasproviders_core).
 
Lopuksi määritetään mitä rajapintaluokkaa asentimen halutaan käyttävän:
$ echo "puppetmaster: true" > config/installer-scenarios.d/default-answers.yaml
Kuten yltä voi arvata, Kafo tukee useita asennusskenaarioita. Asentimen tukemat skenaariot voi listata seuraavasti:
$ bin/puppetmaster-installer --list-scenarios
 Available scenarios
 default (INSTALLED)
Oletusskenaario eli default riittää kuitenkin aivan hyvin useimpiin käyttötapauksiin.
 
Asentimen komentoriviparametrit näkee --help -vivulla:
$ bin/puppetmaster-installer --help
 Usage:
 puppetmaster-installer [OPTIONS]
 
 Options:
 
 = Generic:
 --[no-]colors Use color output on STDOUT (default: true)
 --color-of-background COLOR Your terminal background is :bright or
 :dark (default: :dark)
 --dont-save-answers Skip saving answers to './config/installer-
 scenarios.d/default-answers.yaml'? (default: false)
 --ignore-undocumented Ignore inconsistent parameter documentation
 (default: false)
 -i, --interactive Run in interactive mode
 --log-level LEVEL Log level for log file output (default: "info")
 -n, --noop Run puppet in noop mode? (default: false)
 -p, --profile Run puppet in profile mode? (default: false)
 -s, --skip-checks-i-know-better Skip all system checks (default:
 false)
 -v, --verbose Display log on STDOUT instead of progressbar
 -l, --verbose-log-level LEVEL Log level for verbose mode output
 (default: "info")
 -S, --scenario SCENARIO Use installation scenario
 --disable-scenario SCENARIO Disable installation scenario
 --enable-scenario SCENARIO Enable installation scenario
 --list-scenarios List available installation scenarios
 --force Force change of installation scenario
 --compare-scenarios Show changes between last used scenario and the
 scenario specified with -S or --scenario argument
 --migrations-only Apply migrations to a selected scenario and exit
 --[no-]parser-cache Force use or bypass of Puppet module parser
 cache
 -h, --help print help
 --full-help print complete help
 --[no-]enable-puppetmaster Enable 'puppetmaster' puppet module (default: true)
 
 
 = Module puppetmaster:
 --puppetmaster-puppetserver Setup puppetserver (current: true)
 
 Only commonly used options have been displayed.
 Use --full-help to view the complete list.
Suurin osa parametreista on Kafon luomia geneerisiä parametreja. Rajapintaluokan parametri "puppetserver" on muuntunut asentimen komentoriviparametriksi --puppetmaster-puppetserver, joka voi saada arvokseen true tai false.

Kafo-asennin ei toimi sellaisenaan millä tahansa koneella, vaan se vaatii toimivan Puppet-asennuksen. Tämä ongelma voidaan kiertää käärimällä Kafo-asennin esimerkiksi .deb tai .rpm -pakettiin. Esimerkiksi foreman-installer RPM-paketti paitsi Kafo-installerin, myös sen ajonaikaiset vaatimukset (rubygem-kafo, puppet, rubygem-highline, ruby). Vaihtoehtoisesti asentimen kaveriksi voidaan luoda yksinkertainen skripti, jolla voidaan asentaa tarvittavat paketit.

Toisinaan Puppet-koodissa on tarve tehdä haku Hierasta, mutta se ei toimi odotetulla tavalla. Näissä tapauksissa auttaa "puppet lookup"-komento:

$ puppet lookup --node www.domain.com --explain bacula::filedaemon::backup_files
 Data Binding "hiera"
 Found key: "bacula::filedaemon::backup_files" value: [
 "/etc",
 "/var/lib/puppet/ssl",
 "/var/backups/local"
 ]

Erikoista kyllä, tämä komento löytää ainoastaan ne Hieran hierarkian tasot, jotka perustuvat faktoihin:

---
 :backends:
 - yaml
 :hierarchy:
 - "nodes/%{::trusted.certname}"
 - "roles/%{::role}"
 - "lsbdistcodename/%{::lsbdistcodename}"
 - "osfamily/%{::osfamily}"
 - "kernel/%{::kernel}"
 - common
 
 :yaml:
 :datadir:

Yllä muuttuja role ei siis ole fakta, vaan se on oikeastaan parametri tai käytänne, joka määritetään noodin yaml-tiedostossa:

role: webserver

Nämä yaml-tiedostoissa määritetyt roolit muunnetaan globaaliksi muuttujiksi tiedostossa /etc/puppetlabs/code/environments/production/manifests/site.pp:

# Save server role defined in the node's yaml into a top-scope
 # variable. These top-scope variables are then used in the Hiera
 # hierarchy to configure a node according to it's role.
 $role = hiera('role', undef)

Kun edellä mainitulla "puppet lookup"-komennolla yritetään löytää arvo parametrille, joka on määritelty ainoastaan valitun noodin roolissa, ei sitä löydy. Tämä ongelma voidaan kuitenkin kiertää kankeahkosti luomalla faktatiedosto, esim. facts.yaml ja määrittämällä noodin rooli siinä:

role: 'webserver'

Tämän jälkeen tehdään puppet lookup --facts -parametrin kera:

$ puppet lookup --node www.domain.com --explain bacula::filedaemon::backup_files --facts facts.yaml

Tämä komento etsii myös roolin mukaisesta yaml-tiedostosta. Tämä on erittäin kätevää etenkin, jos halutaan nähdä kaikki noodille määritetyt luokat ja käytössä on edellä mainittu roolien toteutus:

$ puppet lookup --node www.domain.com --explain classes --facts facts.yaml --merge unique

Lisätietoja "puppet lookup"-komennon käytöstä löytyy täältä.

Samuli Seppänen

Toisinaan tulee tarve luoda useita samankaltaisia resursseja, joissa vain jonkin yksittäisen ominaisuuden arvo muuttuu. Esimerkiksi intranetin webbipalvelimeen voi joutua sallimaan yhteydet palomuurin läpi sekä sisäverkon että VPN:n osoiteavaruudesta. Vaikka Puppet ei suoraan tuekaan iteraatiota, sitä voi emuloida antamalla resurssin nimeksi ($title) listan merkkijonon sijaan.

Esimerkiksi Puppet-Finlandin webserver-moduulissa on parametrit $allow_address_ipv4 ja $allow_address_ipv6. Ne voivat saada arvokseen joko merkkijonon ('192.168.55.0/24') tai listan (['192.168.55.0/24', '10.69.0.0/16']):

class webserver::packetfilter
 (
 $allow_address_ipv4,
 $allow_address_ipv6
 
 ) inherits webserver::params

Kummankin parametrin arvo annetaan ::webserver::packetfilter::allow_ip -resurssin nimeksi ($title):

webserver::packetfilter::allow_ip { $allow_address_ipv4:
 provider => 'iptables'
 }
 webserver::packetfilter::allow_ip { $allow_address_ipv6:
 provider => 'ip6tables'
 }

Tämä aiheuttaa sen, että Puppet luo jokaista parametrina annetun listan alkiota kohden yhden annetun tyyppisen resurssin, jonka $title on listan alkion arvo. Webserver-moduulin tapauksessa kutakin alkiota kohden luodaan kaksi palomuurisääntöä:

define webserver::packetfilter::allow_ip
 (
 $provider = 'iptables'
 )
 {
 
 include ::webserver::params
 
 $allow_address = $title
 
 $source = $allow_address ? {
 'any' => undef,
 default => $allow_address,
 }
 
 firewall { "003 accept http from ${allow_address}":
 provider => $provider,
 chain => 'INPUT',
 proto => 'tcp',
 source => $source,
 dport => 80,
 action => 'accept',
 }
 
 firewall { "004 accept https from ${allow_address}":
 provider => $provider,
 chain => 'INPUT',
 proto => 'tcp',
 source => $source,
 dport => 443,
 action => 'accept',
 }
 }

Vastaavaa tekniikkaa voi käyttää myös useamman sisäkkäisen hakemiston luomiseen - alla esimerkki Puppet-Finlandin firefox-moduulista:

file { [ $mozilla_dir,
 $firefox_dir,
 $profiles_dir,
 $profile_dir ]:
 ensure => directory,
 owner => $username,
 }

Puppet-moduulien laatu pysyy korkeana, kun syntaksi- ja tyylivirheitä päästetä edes versionhallintaan asti. Puppet-koodin syntaksivirheet löytyvät helposti:

$ puppet parser validate <tiedosto>

ERB-templateiden virheet saa selvitettyä seuraavalla tavalla:

$ erb -P -x -T '-' <tiedosto>|ruby -c

JSON-tiedostojen (metadata.json) virheet etsiminen vaatii hieman kikkailua:

$ cat <tiedosto>|json_pp -f json -t null

Tyylivirheitä voi etsiä puppet-lint -työkalulla:

$ puppet-lint <tiedosto>
 $ puppet-lint .

Puppet-lint tunnistaa myös puppet-koodin, joka on eteenpäin tai taaksepäin epäyhteensopivaa. Onkin hyvin järkevää ajaa puppet-koodi aina puppet-lintin läpi ennen suuria Puppet-ympäristön päivityksiä.

Puppet-lint osaa paitsi havaita virheitä ja potentiaalisia ongelmia, myös korjata suuren osan niistä:

$ puppet-lint --fix .

Yllä oleva komento korjaa kaikki Puppet-manifestit työhakemistossa ja sen alla olevissa hakemistoissa. Vaikka useimmat puppet-lintin tekemät muutokset ovat turvallisia, voivat tietyt muutokset aiheuttaa toiminnallisia muutoksia koodissa ja noodien konfiguraatiossa. Ongelmien välttämiseksi on syytä lukea versionhallinnan (tässä Git) muutoslogit tarkasti ennen seuraavia Puppet-ajoja:

$ git diff
 --- snip ---
 diff --git a/manifests/loadsql.pp b/manifests/loadsql.pp
 index e4d028f..f0be81d 100644
 --- a/manifests/loadsql.pp
 +++ b/manifests/loadsql.pp
 @@ -28,26 +28,26 @@ define postgresql::loadsql
 )
 {
 file { "postgresql-${basename}.sql":
 - name => "${::postgresql::params::data_dir}/${basename}.sql",
 + ensure => present,
 + name => "${::postgresql::params::data_dir}/${basename}.sql",
 content => template("${modulename}/${basename}.sql.erb"),
 - ensure => present,
 - owner => postgres,
 - group => postgres,
 - mode => 600,
 + owner => postgres,
 + group => postgres,
 + mode => '0600',
 --- snip ---

Yllä olevat muutokset ovat kaikki täysin turvallisia, sillä toiminnallisesti mikään ei muutu. Sen sijaan alla oleva $pg_dump_extra_params -parametrin oletusarvon muutos voisi aiheuttaa ongelmia, etenkin jos kyseistä arvoa käytettäisiin ERB-templatessa:

 define postgresql::backup
 (
 - $ensure = 'present',
 $database,
 + $ensure = 'present',
 $output_dir = '/var/backups/local',
 - $pg_dump_extra_params = '',
 + $pg_dump_extra_params = undef,
 $hour = '01',
 $minute = '10',
 $weekday = '*',

Tässä Puppet-Finlandin postgresql-moduulin tapauksessa tyhjän merkkijonon ('') muuttuminen määrittämättömäksi arvoksi (undef) ei kuitenkaan aiheuta ongelmia.

Muutoslokien lukemisen lisäksi kannattaa ajaa puppet testimoodissa (puppet agent --test --noop), jotta mitään yllättäviä muutoksia ei tule myöskään käytännössä.

Kaikki yllä mainitut syntaksi- ja tyylitarkistukset löytyvät kätevästi paketoituna Puppet-Finlandin puppetmaster-moduulista. Luokka puppetmaster::validation asentaa kullekin tarkistukselle oman skriptinsä sekä erillisen skriptin, joka ajaa kaikki tarkistukset rekursiivisesti työhakemistossa. Lisäksi luokka lisää tarkistukset croniin, jolloin virheistä lähetetään sähköpostia, mikäli niitä moduulikansioon pääsee eksymään.

menucross-circle