How to write your own esxcli plugin

Last week I published an esxcli plugin that allows to run any shell command through esxcli. In this post I want to share what you need to know about the esxcli plugin system to be able to write your own plugins.

The XML interface

When you take a closer look at my esxcli-shell package you will notice that it consists of just two files:
  • /usr/lib/vmware/esxcli/ext/esxcli-shell.xml
  • /opt/VFrontDe/esxcli-shell/bin/esxcli-shell.sh
The esxcli utility lives in /usr/lib/vmware/esxcli, and much of its built-in functionality is implemented as binary libraries that reside in this directory. However, it can be extended through external plugins that can be used to create new namespaces and commands. These plugins are defined by XML files that are stored in the sub directory ext/. The VMware provided esxcli software namespace is implemented in this way, and the associated XML file is esxcli-software.xml.

A (much shorter and clearer) third-party example that I also used for studying the required XML tags and attributes is HP's hpacucli plugin. hpacucli is used for managing HP's Smart Array RAID controllers and was formerly available as a stand-alone utility only. By adding an esxcli plugin for it HP made it remotely accessible and removed the necessity to run the tool inside an ESXi shell. However, this esxcli plugin does nothing more than calling the original stand-alone executable and passing the supplied options to it. So this was a really good example for what I wanted to achieve with my general shell plugin.

I finally ended up with the following contents for the esxcli-shell.xml file:
<?xml version="1.0"?>
<plugin xmlns="http://www.vmware.com/Products/ESX/5.0/esxcli/">
  <version>1.0.0</version>
  
  <namespaces>
    <namespace path="shell">
      <description>Run any ESXi shell command</description>
    </namespace>
  </namespaces>

  <commands>
  
    <command path="shell.cmd">
      <description>Run shell command(s) passed by parameter</description>
      <input-spec>
        <parameter name="command" shortname="c" type="string-list" required="true">
          <description>Shell command(s) to execute</description>
        </parameter>
  <parameter name="logfile" shortname="l" type="string" required="false">
    <description>Specifies a log file to redirect the commands' standard and error output to (see also --errlog)</description>
        </parameter>
  <parameter name="errlog" shortname="e" type="string" required="false">
    <description>Specifies a log file to redirect the commands' error output to (overrides --logfile if also set)</description>
        </parameter>
  <parameter name="workdir" shortname="d" type="string" required="false">
    <description>Specifies a working directory for the shell command(s)</description>
        </parameter>
  <parameter name="lappend" shortname="a" type="flag" required="false">
    <description>Append to existing log file(s) (ignored if --logfile and/or --errlog is not also set)</description>
        </parameter>
      </input-spec>
      <output-spec>
        <string />
      </output-spec>
      <has-updates value="false" />
      <format-parameters>
        <formatter>simple</formatter>
      </format-parameters>
      <execute>/opt/VFrontDe/esxcli-shell/bin/esxcli-shell.sh $multilist{command,-c,true} $if{workdir,-d "$val{workdir}"} $if{logfile,-l "$val{logfile}"} $if{errlog,-e "$val{errlog}"} $if{lappend,-a}</execute>
    </command>
  
    <command path="shell.about">
      <description>About the esxcli shell plugin</description>
      <input-spec />
      <output-spec>
        <string />
      </output-spec>
      <has-updates value="false" />
      <format-parameters>
        <formatter>simple</formatter>
      </format-parameters>
      <execute>/opt/VFrontDe/esxcli-shell/bin/esxcli-shell.sh -i</execute>
    </command>

  </commands>
</plugin>
I assume that you are familiar with the general XML syntax and key terminology, so I'm just going to explain the meaning of the tags and attributes used here. The root element is plugin and in there namespaces (lines 5-9) and commands (lines 11-55) are defined.

Line 6: defines our namespace (this is the first argument of the esxcli call) named "shell". The contents of each description tag is shown in the commands' help texts.
Line 13-40: defines the command "esxcli shell cmd". Valid sub tags are description, input-spec, output-spec, has-updates, format-parameters and execute.
Line 16: The first parameter of the input-spec of the shell.cmd command is "--command" (long name) resp. "-c" (shortname). It is of type string-list and it is mandatory (required="true"). string-list means that the parameter can be used multiple times to specify more than one command string.
Line 19: defines another parameter of shell.cmd's input-spec named "--logfile" (or short "-l"). This time it can only be used once (type string, not string-list) and is optional (required="false").
Line 22, 25: similar definition of "--errlog" ("-e") and "--workdir" ("-d").
Line 28: The last optional parameter "--lappend" ("-a") is of type flag. That means you either specify it (without any additional value) - then its value is true, or you do not specify it - then it is false.
Note: Other possible parameter types are int and int-list. You can also define a constraint for a parameter to limit the acceptable values (examples are in Diagnostic.xml and esxcli-software.xml.
Line 32: The output generated by the plugin is also expected to be in XML format. And with the output-spec tag you define what XML elements the output includes. Our example is quite simple, it just returns a single string (which can span multiple lines). However, you can also define your own XML structures of any complexity. For an example see the definition of software.profile.get in esxcli-software.xml.
Line 35: I have no idea what the has-updates tag means ...
Line 36: The format-parameters tag is about formatting the plugin's output. You can direct esxcli to use different kinds of output formats (check esxcli --help for the --formatter option), e.g. xml or csv, and with the formatter tag you define the default output format (it is set to "simple" in all examples I have seen). There are more sub tags you can use here, and they are related to the output-spec definition. I haven't dived very deep here, because I had no special requirements for the shell plugin.
Line 39: With the execute tag you define what external script or executable the plugin calls to generate the output and how to pass the parameters to them. The $val{} construct references the value of a named parameter, string-lists need to be passed using the $multilist{} construct, and with the $if{} construct you conditionally pass optional parameters only if they have been used.

Starting at line 42 a second command "esxcli shell about" is defined. It's a much more simpler example, because it does not use any parameters, and just calls the shell script with the "-i" option. It is used to print information and a disclaimer for the shell plugin.

You will get the whole picture when we now look at the package's second file, the shell script that is executed by the plugin.

Generating the command output

This is the code of the esxcli-shell.sh script used in the esxcli-shell plugin:
#!/bin/sh
#
TMPSH=/tmp/esxcli-shell.$$
#
while getopts :c:d:l:e:ai opt; do
   case $opt in
   c) echo $OPTARG | sed -e 's/\\\(.\)/\1/g' -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/" >>$TMPSH ;;
   d) wdir=$OPTARG ;;
   l) logfile=$OPTARG ;;
   e) errlog=$OPTARG ;;
   a) appendlog=">" ;;
   i) about=1 ;;
   esac
done
#
echo "<?xml version=\"1.0\" ?>"
echo "<output xmlns=\"http://www.vmware.com/Products/ESX/5.0/esxcli/\">"
echo -n "<string><![CDATA["
#
if [ "$about" == "1" ]; then
   cat <<EOT

Esxcli Shell plugin v1.1 by
   Andreas Peetz (http://www.v-front.de)
...
   possibility of such damages.
EOT
else
   oldPWD="$PWD"
   [ -n "$wdir" ] && cd "$wdir"
   cmd=". $TMPSH"
   [ -n "$logfile" ] && cmd="$cmd >$appendlog\"$logfile\""
   if [ -n "$errlog" ]; then
      cmd="$cmd 2>$appendlog\"$errlog\""
   else
      cmd="$cmd 2>&1"
   fi
   cat $TMPSH | logger -t "shell[$$]: [${VI_USERNAME} via esxcli]"
   ( eval $cmd ) 2>&1 || true
   cd "$oldPWD"
   rm -f $TMPSH
fi
echo "]]></string>"
echo "</output>"
The script is called by esxcli (see the execute tag in the xml file above) and the parameters are passed on to it. They are parsed using the getopts command (lines 5-14).
Command strings that were passed using "-c" will be written into a temporary file after piping them through some sed magic (line 7). esxcli does a very good job with escaping special character in the parameter values. E.g. the shell command ls -la | wc >/tmp/wc.log will be passed as "ls -la \| wc \>/tmp/wc.log". This is why we remove any escaping back slashes (\) and surrounding quotes (') and double quotes (") using sed.
Like mentioned above the script needs to return its output as well-formed XML code, so it starts with printing an XML header (at line 16). The root element is output, and the only tag used inside this is string - this corresponds to the output-spec definition. A CDATA section is used for the output string to prevent it from being interpreted as XML (in case it contains characters like < and > that are used for XML markup).
If "-i" was passed to the script then the "esxcli shell about" command was issued, and it will print out the disclaimer as a so-called Here document (lines 20-43). If not we move on to the else branch of the if statement to handle "esxcli shell cmd" commands:
In line 45-46 the current working directory is first saved to a variable and then changed to the value of "-d" ("--workdir") if it was passed.
Line 47-53: Depending on what parameters have been passed we build a command line ($cmd) here that sources the temporary file created in line 7 and add output redirection as specified.
In line 54 the contents of the temporary script file are piped through the logger utility to send them to the syslog daemon. This way all commands will be recorded in the log file /var/log/shell.log.
Finally the command line ($cmd) that we built will be executed (line 55). After that we return to the original saved working directory, remove the temporary script file and print out the closing XML tags to ensure a well-formed output.

Please note: esxcli is very picky about XML. You must ensure that the script output is well-formed (see e.g. the Wikipedia article on XML) and remember that all XML tags and attributes are case sensitive!

How to package the plugin

I already described in detail how to package a VIB file and an Offline Bundle in the third part of my Daemon's VIB series taking the ProFTPD Offline Bundle as an example.

One important thing to note here is that you can just build an unsigned VIB file of the acceptance level CommunitySupported, because obviously /usr/lib/vmware/esxcli/ext is included in the list of directories that are allowed to be modified by CommunitySupported packages. although it is not mentioned in the list documented by VMware (see this post). (Update 2013-01-19: Just noticed that this is nonsense. The parent directory /usr/lib/vmware is indeed listed there).


No comments:

Post a Comment

***** All comments will be moderated! *****
- Please post only comments or questions that are related to this post's contents!
- Advertising and link spamming will not be tolerated!