38
Configuration surgery with Augeas Raphaël Pinson @raphink LSM 2012, Geneva 2012-07-11 https://github.com/raphink/augeas-talks/

Augeas @RMLL 2012

Embed Size (px)

DESCRIPTION

Augeas talk at RMLL 2012, Geneva

Citation preview

Page 1: Augeas @RMLL 2012

Configuration surgery with AugeasRaphaël Pinson

@raphinkLSM 2012, Geneva

2012-07-11

https://github.com/raphink/augeas-talks/

Page 2: Augeas @RMLL 2012

2/38www.camptocamp.com /

Tired of ugly sed and awk one liners?

or of using tons of different parsing libraries or common::line tricks?

Page 3: Augeas @RMLL 2012

3/38www.camptocamp.com /

Become a configuration surgeon with

Augeas

Page 4: Augeas @RMLL 2012

4/38www.camptocamp.com /

What is the need?

● A lot of different syntaxes● Securely editing configuration files with a

unified API

Page 5: Augeas @RMLL 2012

5/38www.camptocamp.com /

A tree

Augeas turns configuration files into a tree structure:/etc/hosts -> /files/etc/hosts

Page 6: Augeas @RMLL 2012

6/38www.camptocamp.com /

Its branches and leaves

... and their parameters into branches and leaves:augtool> print /files/etc/hosts /files/etc/hosts /files/etc/hosts/1 /files/etc/hosts/1/ipaddr = "127.0.0.1" /files/etc/hosts/1/canonical = "localhost"

Page 7: Augeas @RMLL 2012

7/38www.camptocamp.com /

Augeas provides many stock parsers

They are called lenses:Access Cron Host_ConfAliases Crypttab HostnameAnacron debctrl Hosts_AccessApprox Desktop IniFileAptConf Dhcpd InputrcAutomaster Dpkg IptablesAutomounter Exports KdumpBackupPCHosts FAI_DiskConfig Keepalivedcgconfig Fonts Keepalivedcgrules Fuse Login_defsChannels Grub Mke2fs...

Page 8: Augeas @RMLL 2012

8/38www.camptocamp.com /

... as well as generic lenses

available to build new parsers:Build Sep SimplelinesIniFile Shellvars SimplevarsRx Shellvars_list Util

Page 9: Augeas @RMLL 2012

9/38www.camptocamp.com /

augtool lets you inspect the tree

$ augtool

augtool> ls / augeas/ = (none) files/ = (none)

augtool> print /files/etc/passwd/root/ /files/etc/passwd/root /files/etc/passwd/root/password = "x" /files/etc/passwd/root/uid = "0" /files/etc/passwd/root/gid = "0" /files/etc/passwd/root/name = "root" /files/etc/passwd/root/home = "/root" /files/etc/passwd/root/shell = "/bin/bash"

Page 10: Augeas @RMLL 2012

10/38www.camptocamp.com /

The tree can be queried using XPath

augtool> print /files/etc/passwd/*[uid='0'][1] /files/etc/passwd/root /files/etc/passwd/root/password = "x" /files/etc/passwd/root/uid = "0" /files/etc/passwd/root/gid = "0" /files/etc/passwd/root/name = "root" /files/etc/passwd/root/home = "/root" /files/etc/passwd/root/shell = "/bin/bash"

Page 11: Augeas @RMLL 2012

11/38www.camptocamp.com /

But also modified

$ getent passwd rootroot:x:0:0:root:/root:/bin/bash

$ augtool

augtool> set /files/etc/passwd/*[uid='0']/shell /bin/shaugtool> match /files/etc/passwd/*[uid='0']/shell/files/etc/passwd/root/shell = "/bin/sh"augtool> saveSaved 1 file(s)augtool> exit

$ getent passwd rootroot:x:0:0:root:/root:/bin/sh

Page 12: Augeas @RMLL 2012

12/38www.camptocamp.com /

Puppet has a native provider

augeas {'export foo': context => '/files/etc/exports', changes => [ "set dir[. = '/foo'] /foo", "set dir[. = '/foo']/client weeble", "set dir[. = '/foo']/client/option[1] ro", "set dir[. = '/foo']/client/option[2] all_squash", ],}

Page 13: Augeas @RMLL 2012

13/38www.camptocamp.com /

It is better to wrap things up

define kmod::generic( $type, $module, $ensure=present, $command='', $file='/etc/modprobe.d/modprobe.conf') { augeas {"${type} module ${module}": context => "/files${file}", changes => [ "set ${type}[. = '${module}'] ${module}", "set ${type}[. = '${module}']/command '${command}'", ], }}

Page 14: Augeas @RMLL 2012

14/38www.camptocamp.com /

mcollective has an agent

$ mco augeas match /files/etc/passwd/rpinson/shell

* [ ======================================> ] 196 / 196

...wrk1saja-map-dev /files/etc/passwd/rpinson/shell = /bin/bashwrk3wrk4 /files/etc/passwd/rpinson/shell = /bin/bash...

Page 15: Augeas @RMLL 2012

15/38www.camptocamp.com /

... and uses it for discovery

$ mco find -S "augeas_match(/files/etc/passwd/rip).size = 0"

Page 16: Augeas @RMLL 2012

16/38www.camptocamp.com /

Bindings include Perl, Python, Java, PHP, Haskell, Ruby...

require 'augeas'aug = Augeas.openif aug.match('/augeas/load'+lens).length > 0 aug.set('/augeas/load/'+lens+'incl[last()+1]', path)else aug.set('/augeas/load/'+lens+'/lens', lens+'.lns')end

(From the mcollective agent)

Page 17: Augeas @RMLL 2012

17/38www.camptocamp.com /

The Ruby bindings can be used in Facter

Facter.add(:augeasversion) do setcode do begin require 'augeas' aug = Augeas::open('/', nil, Augeas::NO_MODL_AUTOLOAD) ver = aug.get('/augeas/version') aug.close ver rescue Exception Facter.debug('ruby-augeas not available') end endend

(From the augeasversion fact)

Page 18: Augeas @RMLL 2012

18/38www.camptocamp.com /

Or to write native types

def ip aug = nil path = "/files#{self.class.file(resource)}" begin aug = self.class.augopen(resource) aug.get("#{path}/*[canonical = '#{resource[:name]}']/ipaddr") ensure aug.close if aug endend

(See https://github.com/domcleal/augeasproviders)

Page 19: Augeas @RMLL 2012

19/38www.camptocamp.com /

The case of sshd_configCustom type:define ssh::config::sshd ($ensure='present', $value='') {

case $ensure { 'present': { $changes = "set ${name} ${value}" }

'absent': { $changes = "rm ${name}" }

'default': { fail("Wrong value for ensure: ${ensure}") } }

augeas {"Set ${name} in /etc/ssh/sshd_config": context => '/files/etc/ssh/sshd_config', changes => $changes, }}

Page 20: Augeas @RMLL 2012

20/38www.camptocamp.com /

Using the custom type for sshd_config

ssh::config::sshd {'PasswordAuthenticator': value => 'yes',}

Page 21: Augeas @RMLL 2012

21/38www.camptocamp.com /

The problem with sshd_config

Match groups:Match Host example.com PermitRootLogin no

=> Not possible with ssh::config::sshd, requires insertions and looping through the configuration parameters.

Page 22: Augeas @RMLL 2012

22/38www.camptocamp.com /

A native provider for sshd_config (1)The type:Puppet::Type.newtype(:sshd_config) do ensurable

newparam(:name) do desc "The name of the entry." isnamevar end

newproperty(:value) do desc "Entry value." end

newproperty(:target) do desc "File target." end

newparam(:condition) do desc "Match group condition for the entry." endend

Page 23: Augeas @RMLL 2012

23/38www.camptocamp.com /

A native provider for sshd_config (2)

The provider:require 'augeas' if Puppet.features.augeas?

Puppet::Type.type(:sshd_config).provide(:augeas) do desc "Uses Augeas API to update an sshd_config parameter"

def self.file(resource = nil) file = "/etc/ssh/sshd_config" file = resource[:target] if resource and resource[:target] file.chomp("/") end

confine :true => Puppet.features.augeas? confine :exists => file

Page 24: Augeas @RMLL 2012

24/38www.camptocamp.com /

A native provider for sshd_config (3)def self.augopen(resource = nil) aug = nil file = file(resource) begin aug = Augeas.open(nil, nil, Augeas::NO_MODL_AUTOLOAD) aug.transform( :lens => "Sshd.lns", :name => "Sshd", :incl => file ) aug.load!

if aug.match("/files#{file}").empty? message = aug.get("/augeas/files#{file}/error/message") fail("Augeas didn't load #{file}: #{message}") end rescue aug.close if aug raise end augend

Page 25: Augeas @RMLL 2012

25/38www.camptocamp.com /

A native provider for sshd_config (4)def self.instances aug = nil path = "/files#{file}" entry_path = self.class.entry_path(resource) begin resources = [] aug = augopen aug.match(entry_path).each do |hpath| entry = {} entry[:name] = resource[:name] entry[:conditions] = Hash[*resource[:condition].split(' ').flatten(1)] entry[:value] = aug.get(hpath)

resources << new(entry) end resources ensure aug.close if aug endend

Page 26: Augeas @RMLL 2012

26/38www.camptocamp.com /

A native provider for sshd_config (5)def self.match_conditions(resource=nil) if resource[:condition] conditions = Hash[*resource[:condition].split(' ').flatten(1)] cond_keys = conditions.keys.length cond_str = "[count(Condition/*)=#{cond_keys}]" conditions.each { |k,v| cond_str += "[Condition/#{k}=\"#{v}\"]" } cond_str else "" endend

def self.entry_path(resource=nil) path = "/files#{self.file(resource)}" if resource[:condition] cond_str = self.match_conditions(resource) "#{path}/Match#{cond_str}/Settings/#{resource[:name]}" else "#{path}/#{resource[:name]}" endend

Page 27: Augeas @RMLL 2012

27/38www.camptocamp.com /

A native provider for sshd_config (6)

def self.match_exists?(resource=nil) aug = nil path = "/files#{self.file(resource)}" begin aug = self.augopen(resource) if resource[:condition] cond_str = self.match_conditions(resource) else false end not aug.match("#{path}/Match#{cond_str}").empty? ensure aug.close if aug endend

Page 28: Augeas @RMLL 2012

28/38www.camptocamp.com /

A native provider for sshd_config (7)def exists? aug = nil entry_path = self.class.entry_path(resource) begin aug = self.class.augopen(resource) not aug.match(entry_path).empty? ensure aug.close if aug endend

def self.create_match(resource=nil, aug=nil) path = "/files#{self.file(resource)}" begin aug.insert("#{path}/*[last()]", "Match", false) conditions = Hash[*resource[:condition].split(' ').flatten(1)] conditions.each do |k,v| aug.set("#{path}/Match[last()]/Condition/#{k}", v) end aug endend

Page 29: Augeas @RMLL 2012

29/38www.camptocamp.com /

A native provider for sshd_config (8)def create aug = nil path = "/files#{self.class.file(resource)}" entry_path = self.class.entry_path(resource) begin aug = self.class.augopen(resource) if resource[:condition] unless self.class.match_exists?(resource) aug = self.class.create_match(resource, aug) end else unless aug.match("#{path}/Match").empty? aug.insert("#{path}/Match[1]", resource[:name], true) end end aug.set(entry_path, resource[:value]) aug.save! ensure aug.close if aug endend

Page 30: Augeas @RMLL 2012

30/38www.camptocamp.com /

A native provider for sshd_config (9)

def destroy aug = nil path = "/files#{self.class.file(resource)}" begin aug = self.class.augopen(resource) entry_path = self.class.entry_path(resource) aug.rm(entry_path) aug.rm("#{path}/Match[count(Settings/*)=0]") aug.save! ensure aug.close if aug endend

def target self.class.file(resource)end

Page 31: Augeas @RMLL 2012

31/38www.camptocamp.com /

A native provider for sshd_config (10)

def value aug = nil path = "/files#{self.class.file(resource)}" begin aug = self.class.augopen(resource) entry_path = self.class.entry_path(resource) aug.get(entry_path) ensure aug.close if aug endend

Page 32: Augeas @RMLL 2012

32/38www.camptocamp.com /

A native provider for sshd_config (11)

def value=(thevalue) aug = nil path = "/files#{self.class.file(resource)}" begin aug = self.class.augopen(resource) entry_path = self.class.entry_path(resource) aug.set(entry_path, thevalue) aug.save! ensure aug.close if aug endend

Page 33: Augeas @RMLL 2012

33/38www.camptocamp.com /

Using the native provider for sshd_config

sshd_config {'PermitRootLogin': ensure => present, condition => 'Host example.com', value => 'yes',}

Page 34: Augeas @RMLL 2012

34/38www.camptocamp.com /

Errors are reported in the /augeas tree

augtool> print /augeas//error /augeas/files/etc/mke2fs.conf/error = "parse_failed" /augeas/files/etc/mke2fs.conf/error/pos = "82" /augeas/files/etc/mke2fs.conf/error/line = "3" /augeas/files/etc/mke2fs.conf/error/char = "0" /augeas/files/etc/mke2fs.conf/error/lens = \ "/usr/share/augeas/lenses/dist/mke2fs.aug:132.10-.49:" /augeas/files/etc/mke2fs.conf/error/message = \ "Get did not match entire input"

Page 35: Augeas @RMLL 2012

35/38www.camptocamp.com /

Other projects using Augeas

● libvirt● rpm● Nut● guestfs● ZYpp● Config::Model● Augeas::Validator

Page 36: Augeas @RMLL 2012

36/38www.camptocamp.com /

Future projects

● more API calls● improved XPath syntax● more lenses● more native providers● DBUS provider● content validation in Puppet (validator)● integration in package managers● finish the Augeas book● ...● your idea/project here...

Page 37: Augeas @RMLL 2012

37/38www.camptocamp.com /

Questions?

http://[email protected]

freenode: #augeas

Page 38: Augeas @RMLL 2012