Another in the series of the fun things that are new about CentOS 7: setting up an iSCSI target. Asquith used to do this with this sort of code:
lvcreate -l 70%VG -n asquith-dbdata vg1 echo "<target iqn.2013-08.home.dizwell:asquith.dbdata>" >> /etc/tgt/targets.conf echo " backing-store /dev/vg1/asquith-dbdata" >> /etc/tgt/targets.conf echo "</target>" >> /etc/tgt/targets.conf chkconfig tgtd on service tgtd start
This is all good, old-fashioned stuff involving writing something to a configuration file and then starting a daemon to use it.
But this is not how we do it now. Oh no. In CentOS 7 (and its Red Hat-y and Oracle-y equivalents, of course), we use an interactive tool called targetcli to invoke an independent ‘shell’ in which these sorts of commands are issued instead:
backstores/block create name=dbdata dev=/dev/mapper/vg1-asquith--dbdata iscsi/ create iqn.2014-07.home.dizwell:asquith.dbdata cd iscsi/iqn.2014-07.home.dizwell:asquith.dbdata/tpg1 portals/ create luns/ create /backstores/block/dbdata set attribute authentication=0 set attribute generate_node_acls=1 exit
…and the final exit there takes you back to your original shell. (I probably should say that targetcli itself is not actually that new, having first been released in about 2009… but it’s now the default way of doing things in the 7 release of Enterprise Linux, and that’s new -at least, to me!).
Anyway, targetcli is definitely nice and easy and there are no services to worry about: it all just starts getting shared by magic. About the only way to check anything is actually working after you’ve issued all those targetcli commands is to do:
…before and after. If port 3260 is not in use before, but is in use afterwards, then you know it’s working properly.
The real bummer about the new technique, however, is that it’s not really very scriptable. It’s an interactive tool after all, and appears to expect a system admin to be sitting at the end of the keyboard… which is not much use if you’re trying to get this all to happen as part of a Kickstart auto-build, say!
I did work out that the old trick of piping things together will help. For example, if the above commands are re-written slightly to be:
echo "cd /" | targetcli echo "backstores/block create name=dbdata dev=/dev/mapper/vg1-asquith--dbdata" | targetcli echo "iscsi/ create iqn.2014-07.home.dizwell:asquith.dbdata" | targetcli echo "cd iscsi/iqn.2014-07.home.dizwell:asquith.dbdata/tpg1" | targetcli echo "portals/ create" | targetcli echo "luns/ create /backstores/block/dbdata" | targetcli echo "set attribute authentication=0" | targetcli echo "set attribute generate_node_acls=1" | targetcli
…then each of the commands in double-quotes will be passed through to the targetcli shell in turn and executed in just the same way as if you’d typed things interactively.
Excellent… but I need these commands in a slightly different context. What I’m after is for Kickstart to create a shell script that contains these commands so that it can then execute that shell script later on to automagically set up iSCSI target sharing when building a CentOS 7 Asquith Server. That means I need Kickstart to run commands which create a script which contains these commands. And at that point, I’m asking for the commands to be re-written in the following manner:
echo "echo \"cd /\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"backstores/block create name=dbdata dev=/dev/mapper/vg1-asquith--dbdata\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"iscsi/ create iqn.2014-07.home.dizwell:asquith.dbdata\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"cd iscsi/iqn.2014-07.home.dizwell:asquith.dbdata/tpg1\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"portals/ create\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"luns/ create /backstores/block/dbdata\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"set attribute authentication=0\" | targetcli" >> /root/iscsiconfig.sh echo "echo \"set attribute generate_node_acls=1\" | targetcli" >> /root/iscsiconfig.sh
The levels of indirection here start to do my head in!
Take the first command: echo “echo \”cd /\“ | targetcli” » /root/iscsiconfig.sh
That’s my earlier echo-and-pipe-to-targetcli command wrapped up in an echo command of its own. Why? Because Kickstart will perform the ‘outer echo’ and thus write a line of text reading just echo “cd /” | targetcli to a shell script called iscsiconfig.sh. So when Kickstart later runs iscsiconfig.sh, the correct targetcli command is finally run.
So basically, we’re nesting an echo inside an echo. Kickstart will run the ‘outer echo’ so that the ‘inner echo’ command gets written to a script file. Only when that script is itself later run will the ‘inner echo’ actually be run and do anything.
Now, the way I’ve written my original inner echoes is perhaps peculiar to me: double quotes make visual sense to me and there’s no major difference in Bash between scripting with double or single quotes. Without a major functional difference, I prefer doubles. But if you start nesting your echoes, the “inner echo” has to escape its double quotes, otherwise they get taken literally, as characters, not command delimiters.
In other words, the command we eventually want to run might be:
echo "cd /" | targetcli
…where the double-quotes are not escaped, because they delimit what is to be echoed. But the command we have to run to get this command written into a shell script is:
echo "echo \"cd /\" | targetcli" >> /root/iscsiconfig.sh
So the ‘outer’ echo is now a command to write the words echo “<something>“ into a shell script -but the double-quotes used by this inner echo have to be regarded as literal text, not as parts of the outer echo command. Hence they need to be preceded by a “\” to turn them into literals (“escaped”, in the lingo).
Is your head hurting yet?! It gets worse (a bit)!
Remember that these echoed echo commands are being written into a shell script. Shell scripts need to start with a line saying where the shell executables are to be found. In the world of the Bourne Again Shell, that means starting things with a line which reads:
Now, we want a command that echoes that into a shell script, before we later go on to execute the shell script. No problems …we just do this:
echo "#!/bin/bash" > /root/iscsiconfig.sh
This is merely as before: we’re wrapping the command we eventually want executed inside an echo statement, using double quotes as delimiters to define what gets echoed, and finishing off with a redirection out to the shell script that will contain the command.
Except that it won’t work:
[[email protected] ~]# echo "#!/bin/bash" > /root/iscsiconfig.sh -bash: !/bin/bash": event not found
You might think that one or more of the “shebang” characters (the ”#!” at the start of the line being echoed) need escaping, so that something like
echo "\#\!/bin/bash" > /root/iscsiconfig.sh
…would do the trick. And indeed, the above escaped command will “work” instead of producing an ‘event not found’ error, but it doesn’t work very well! Just try displaying the contents of the file created with that modified command:
[[email protected] ~]# cat iscsiconfig.sh \#\!/bin/bash
This shows you that the escape characters have actually become part of the contents of the shell script, rather than interpreted as escape characters. Present as literals, though, the escape characters mean the shell script can’t actually work when invoked. So this won’t do.
Odd though it might seem at first sight, this behaviour is actually perfectly cromulant and well-documented in the Bash manual. Specifically:
A double quote may be quoted within double quotes by preceding it with a backslash. If enabled, history expansion will be performed unless an ‘!’ appearing in double quotes is escaped using a backslash. The backslash preceding the ‘!’ is not removed.
So what’s the fix? Well, the simplest I can think of is to …er, use single quotes. You’ll find that:
echo '#!/bin/bash' > /root/iscsiconfig.sh
…works in the sense of not itself returning an error AND works in the sense that it writes the correct command into the shell script file we’re trying to create.
But now, at this point, you realise it’s a bit silly to have a mix of single and double-quotes in the same set of commands, so you think that you could go back to the original targetcli commands and re-write them using single quotes (after all, the manual makes it clear that there’s no real difference between single and double quotes except for the way four literal characters are treated).
This means your ‘doing it directly’ commands would be written as:
echo 'cd /' | targetcli echo 'backstores/block create name=dbdata dev=/dev/mapper/vg1-asquith--dbdata' | targetcli echo 'iscsi/ create iqn.2014-07.home.dizwell:asquith.dbdata' | targetcli echo 'cd iscsi/iqn.2014-07.home.dizwell:asquith.dbdata/tpg1' | targetcli echo 'portals/ create' | targetcli echo 'luns/ create /backstores/block/dbdata' | targetcli echo 'set attribute authentication=0' | targetcli echo 'set attribute generate_node_acls=1' | targetcli
And they do, indeed, all work as advertised in this form. But now you want to apply that ‘layer of indirection’ that comes from the fact that you’re writing a script to write a script… so you might end up with this:
echo '#!/bin/bash' > /root/iscsiconfig.sh echo 'echo 'cd /' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'backstores/block create name=dbdata dev=/dev/mapper/vg1-asquith--dbdata' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'iscsi/ create iqn.2014-07.home.dizwell:asquith.dbdata' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'cd iscsi/iqn.2014-07.home.dizwell:asquith.dbdata/tpg1' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'portals/ create' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'luns/ create /backstores/block/dbdata' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'set attribute authentication=0' | targetcli' >> /root/iscsiconfig.sh echo 'echo 'set attribute generate_node_acls=1' | targetcli' >> /root/iscsiconfig.sh
I have to say, I don’t like the look of that, because gut instinct tells me double-quotes are needed there somewhere (and since, in this case, single and double quotes are interchangeable, there’s no reason why you couldn’t come up with a rule that says ‘inner echoes get doubles, outer echoes use singles’ and thus end up with something that looks a bit clearer than the above and yet remains consistent!).
But it will all work.
Which is the main thing
Though my head still hurts!
(Incidentally, you don’t need to point knowingly, laughing your head off all the while, exclaiming that I should have used a here document technique to avoid all those nested echoes in the first place. It’s true and I know (and knew) it. But lots of single-line echoes are how I start writing stuff. Only when I know it works do I start applying layers of ‘elegance’ by code tidy-ups such as that. This blog was dedicated to those whose heads hurt, not those who can write shell scripts more elegantly than me… there are far too many of those!)