Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Hi Guys,

So we have ~ 2k machines configured via Puppet.
These have some ssh keys set that are now redundant and need removing

I inherited the following (minimized) code
define line($file, $line, $ensure = 'present') { case $ensure { default: { err ( "unknown ensure value ${ensure}" ) } present: { exec { "/bin/echo '${line}' >> '${file}'": unless => "/bin/grep -qFx -- '${line}' + '${file}'" } } absent: { exec { "/usr/bin/perl -ni -e 'print unless /^\ +\Q${line}\\E\$/' '${file}'": onlyif => "/bin/grep -qFx -- '${line}' + '${file}'" } } } } line { 'ssh_key' : file => '/root/user/b1.bak', line => 'ssh-rsa AAAA/BBBB/knMQ== user@host.f.q.d.n', ensure => absent, }

but it seems it can't cope with special chars like '/'/ & '@'; test run shows
2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): Having no space bet +ween pattern and following word is deprecated at -e line 1. 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): Bareword found wher +e operator expected at -e line 1, near "/^\Qssh-rsa AAAA/BBBB" 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): (Missing operat +or before BBBB?) 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): Array found where o +perator expected at -e line 1, at end of line 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): (Missing operat +or before ?) 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): Bareword found wher +e operator expected at -e line 1, near "q.d.n" 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): Backslash found whe +re operator expected at -e line 1, near "n\" 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): syntax error at -e +line 1, near "/^\Qssh-rsa AAAA/BBBB" 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (notice): Execution of -e abo +rted due to compilation errors. 2019-11-27 21:49:14 +0000 Puppet (err): /usr/bin/perl -ni -e 'print un +less /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f.q.d.n\E$/' '/root/user/ +b1.bak' returned 255 instead of one of [0] 2019-11-27 21:49:14 +0000 /Stage[main]/Main/Line[ssh_key]/Exec[/usr/bi +n/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/knMQ== user@host.f. +q.d.n\E$/' '/root/user/b1.bak']/returns (err): change from notrun to +0 failed: /usr/bin/perl -ni -e 'print unless /^\Qssh-rsa AAAA/BBBB/kn +MQ== user@host.f.q.d.n\E$/' '/root/user/b1.bak' returned 255 instead +of one of [0]

I have tried various ways of quoting/escaping etc, but none seem to work.
I would definitely appreciate a working solution

Cheers
Chris

Replies are listed 'Best First'.
Re: One liner: remove ssh keys with quotemeta
by hippo (Archbishop) on Nov 28, 2019 at 09:37 UTC

    I don't use puppet either. However, I do not see the point in using regex for this:

    /usr/bin/perl -ni -e 'print unless /^\\Q${line}\\E\$/' '${file}'

    where it appears that you are just wanting an exact match on the entire line. While quotemeta will get you so far, remember that you are substituting ${line} in at the shell level, so any regex-terminating characters within that variable will also have to be escaped somehow. Much better to use an exact match I would have thought.

    Similarly, you could specify the line to remove in an env var, thus avoiding one layer of escaping.

    Without puppet and just using STDIN for clarity, here is a demo of both:

    $ export LINE=bar $ echo -e "foo\nbar\nbaz" | perl -lne 'print unless $_ eq $ENV{LINE}'

    You'll still need to sanitize/escape/quote your equivalent of "bar" when setting the environment variable but that should be a simpler task.

      remember that you are substituting ${line} in at the shell level, so any regex-terminating characters within that variable will also have to be escaped somehow

      And here's my pet problem again: The problem of "the" default shell. It does not only affect perl, it is a general problem. And the solution is also generic - avoid the shell! From five minutes of Google, it seems that Puppet is written in Ruby. Ruby inherits many ideas from Perl, including a multi-argument form of exec(), including guesswork to avoid the shell when called with a single argument.

      So the sane way to get the job done is to use a multi-argument exec() already at the Puppet level, i.e. exec "perl","-E","say 'your code here'".

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: One liner: remove ssh keys with quotemeta
by soonix (Chancellor) on Nov 28, 2019 at 08:25 UTC
    There are at least three systems involved with quoting: Puppet, the Shell, and Perl. I don't know anything about Puppet, but you could add some exec { "/bin/echo line='${line}' file='${file}'" } to see what Perl will get if invoked.
Re: One liner: remove ssh keys with quotemeta
by jcb (Parson) on Nov 28, 2019 at 22:00 UTC

    This is another case where Perl probably is not the right tool for the job and you would be better off using sed than bringing up an entire perl instance just for one exact match. Try something like:

    exec { "/bin/sed -i -e '\\%${line}%d' '${file}'" : ... }

    Your problem seems to be the presence of the regex delimiter in some of the data that you need to match. Try using a different delimiter — in Perl /$pat/ is shorthand for m/$pat/ and you can replace it with m[$pat]. See perlsyn and perlop for more details.

      exec { "/bin/sed -i -e '\\%${line}%d' '${file}'" : ... }

      That would still suffer from the same problem as perl - the default shell. See Re^2: One liner: remove ssh keys with quotemeta.

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

        While you are correct that the default shell can be a strange critter, the questioner's existing code assumes Bourne-like shell quoting rules, so I offered an example that follows suit.

        A better answer would probably be to write the update routines entirely in Ruby instead of calling out to other programs, but this is PerlMonks, not RubyMonks. :-)

Re: One liner: remove ssh keys with quotemeta
by Anonymous Monk on Nov 29, 2019 at 05:28 UTC
    Hi Guys,
    so, getting it to the cmd line wasn't the problem, it was the interpretation by Perl eg
    Puppet out: Debug: Exec[line_remove](provider=posix): Executing '/usr/bin/perl -w +-ni -e 'my $qs = quotemeta('ssh-rsa AAAA/BBBB/knMQ== user@host.f.q.d. +n'); print unless /^$qs$/ ' '/root/user/b1.bak'' Debug: Executing: '/usr/bin/perl -w -ni -e 'my $qs = quotemeta('ssh-rs +a AAAA/BBBB/knMQ== user@host.f.q.d.n'); print unless /^$qs$/ ' '/roo +t/user/b1.bak'' Notice: /Stage[main]/Main/Line[ssh_key]/Exec[line_remove]/returns: Unq +uoted string "ssh" may clash with future reserved word at -e line 1. Notice: /Stage[main]/Main/Line[ssh_key]/Exec[line_remove]/returns: syn +tax error at -e line 1, at EOF Notice: /Stage[main]/Main/Line[ssh_key]/Exec[line_remove]/returns: Exe +cution of -e aborted due to compilation errors. ## Same errors as direct at the cmd line # /usr/bin/perl -w -ni -e 'my $qs = quotemeta('ssh-rsa AAAA/BBBB/knMQ= += user@host.f.q.d.n'); print unless /^$qs$/ ' '/root/user/b1.bak' Unquoted string "ssh" may clash with future reserved word at -e line 1 +. syntax error at -e line 1, at EOF Execution of -e aborted due to compilation errors. # Try swap outer quotes '-> " # /usr/bin/perl -w -ni -e "my $qs = quotemeta('ssh-rsa AAAA/BBBB/knMQ= += user@host.f.q.d.n'); print unless /^$qs$/ " '/root/user/b1.bak' syntax error at -e line 1, near "my =" Execution of -e aborted due to compilation errors. # Try "" around quotemeta string # /usr/bin/perl -w -ni -e 'my $qs = quotemeta("ssh-rsa AAAA/BBBB/knMQ= += user@host.f.q.d.n"); print unless /^$qs$/ ' '/root/user/b1.bak' Possible unintended interpolation of @host in string at -e line 1. Name "main::host" used only once: possible typo at -e line 1.

    However, using the post by gam3 here https://www.perlmonks.org/?node_id=438860 ('general soln'), it seemed to work
    # Direct cli # perl -w -ni -e 'my $str = sprintf( qq(%s), q(ssh-rsa AAAA/BBBB/knMQ= += user@host.f.q.d.n) ); print $_ unless $_ =~ /$str/ ' /root/user/b1 +.bak # From within puppet command => "/usr/bin/perl -w -ni -e 'my \$str = sprintf( qq(%s), q($li +ne) ); print \$_ unless \$_ =~ /\$str/ ' '${file}'",

    But a full ssh key has a '+' char as well, and that fails to match :(
    # cat b1.bak ssh-rsa CCCC/DDDD/knMQ== user@host2.f.q.d.n ssh-rsa AAAA/BBBB/kPW+Yi9yZ7Kh0mL/knMQ== user@host.f.q.d.n # perl -w -n -e 'my $str = sprintf( qq(%s), q(ssh-rsa AAAA/BBBB/kPW+Y +i9yZ7Kh0mL/knMQ== user@host.f.q.d.n) ); print $_ unless $_ =~ /$str/ +' /root/user/b1.bak ssh-rsa CCCC/DDDD/knMQ== user@host2.f.q.d.n ssh-rsa AAAA/BBBB/kPW+Yi9yZ7Kh0mL/knMQ== user@host.f.q.d.n

    So close ... damn it ... looks like I DO need to get quotemeta() working...
    An ideas?

    Cheers
    Chris

    BTW, when I write the post for perlmonks, it uses all the whitespace, but when I actually commit the post, it crams all code stuff to one side; why ?

      As per my previous reply, discarding the regex and just using string equality:

      $ cat b1.bak ssh-rsa CCCC/DDDD/knMQ== user@host2.f.q.d.n ssh-rsa AAAA/BBBB/kPW+Yi9yZ7Kh0mL/knMQ== user@host.f.q.d.n $ perl -lne 'print unless $_ eq q(ssh-rsa AAAA/BBBB/kPW+Yi9yZ7Kh0mL/kn +MQ== user@host.f.q.d.n)' b1.bak ssh-rsa CCCC/DDDD/knMQ== user@host2.f.q.d.n
        Hey mate,

        thanks for that.
        It turns out you were right and in fact we don't need to do all the escapes / quotemeta stuff !
        I can't believe it's that simple - I was convinced the previous guy was on the right track...

        To be fair, I've done a lot of Perl programming in the past, but almost never one liners like this (especially also having to worry about passing it through the shell etc).

        Final code (inside puppet)
        exec { "/usr/bin/perl -i -lne 'print unless \$_ eq q($line)' '${file} +'":
        BTW, that -l switch is magic :)

        Cheers
        Chris

      If you insist on this approach, try inserting quotemeta just before sprintf.