Main Window Side Panels and Menu Titles
Non-Standard BUI Control Primer
Returning to a Previous Context
Navigating to a Parent Context
Executing Context-Specific Commands
Getting a Single Property Value
Committing a Set Property Value
Setting a Property Value with an Implied Commit
Setting a Property to a List of Values
Setting a Property to a Value Containing Special Characters
The simplest scripting mechanism is to batch appliance shell commands. For example, to automatically take a snapshot called "newsnap" in the project "myproj" and the filesystem "myfs", put the following commands in a file:
shares select myproj select myfs snapshots snapshot newsnap
Then ssh onto the appliance, redirecting standard input to be the file:
% ssh root@dory < myfile.txt
In many shells, you can abbreviate this by using a "here file", where input up to a token is sent to standard input. Following is the above example in terms of a here file:
% '''ssh root@dory << EOF shares select myproj select myfs snapshots snapshot newsnap EOF'''
This mechanism is sufficient for the simplest kind of automation, and may be sufficient if wrapped in programmatic logic in a higher-level shell scripting language on a client, but it generally leaves much to be desired.
While batching commands is sufficient for the simplest of operations, it can be tedious to wrap in programmatic logic. For example, if you want to get information on the space usage for every share, you must have many different invocations of the CLI, wrapped in a higher level language on the client that parsed the output of specific commands. This results in slow, brittle automation infrastructure. To allow for faster and most robust automation, the appliance has a rich scripting environment based on ECMAScript 3. An ECMAScript tutorial is beyond the scope of this document, but it is a dynamically typed language with a C-like syntax that allows for:
Conditional code flow (if/else)
Iterative code flow (while, for, etc.)
Structural and array data manipulation via first-class Object and Array types
Perl-like regular expressions and string manipulation (split(), join(), etc.)
Exceptions
Sophisticated functional language features like closures
In the CLI, enter the script environment using the script command:
dory:> script
("." to run)>
As the script environment prompt, you can input your script, finally entering "." alone on a line to execute it:
dory:> script
("." to run)> for (i = 10; i > 0; i--)
("." to run)> printf("%d... ", i);
("." to run)> printf("Blastoff!\n");
("." to run)> .
10... 9... 8... 7... 6... 5... 4... 3... 2... 1... Blastoff!
If your script is a single line, you can simply provide it as an argument to the script command, making for an easy way to explore scripting:
dory:> script print("It is now " + new Date())
It is now Tue Oct 14 2009 05:33:01 GMT+0000 (UTC)
Of course, scripts are of little utility unless they can interact with the system at large. There are several built-in functions that allow your scripts to interact with the system:
|
The simplest way for scripts to interact with the larger system is to use the "run" function: it takes a command to run, and returns the output of that command as a string. For example:
dory:> configuration version script dump(run('get boot_time'))
' boot_time = 2009-10-12 07:02:17\n'
The built-in dump function dumps the argument out, without expanding any embedded newlines. ECMAScript's string handling facilities can be used to take apart output. For example, splitting the above based on whitespace:
dory:> configuration version script dump(run('get boot_time').split(/\s+/))
['', 'boot_time', '=', '2009-10-12', '07:02:17', '']
The run function is sufficiently powerful that it may be tempting to rely exclusively on parsing output to get information about the system -- but this has the decided disadvantage that it leaves scripts parsing human-readable output that may or may not change in the future. To more robustly gather information about the system, use the built-in "get" function. In the case of the boot_time property, this will return not the string but rather the ECMAScript Date object, allowing the property value to be manipulated programmatically. For example, you might want to use the boot_time property in conjunction with the current time to determine the time since boot:
script
run('configuration version');
now = new Date();
uptime = (now.valueOf() - get('boot_time').valueOf()) / 1000;
printf('up %d day%s, %d hour%s, %d minute%s, %d second%s\n',
d = uptime / 86400, d < 1 || d >= 2 ? 's' : '',
h = (uptime / 3600) % 24, h < 1 || h >= 2 ? 's': '',
m = (uptime / 60) % 60, m < 1 || m >= 2 ? 's': '',
s = uptime % 60, s < 1 || s >= 2 ? 's': '');
Assuming the above is saved as a "uptime.aksh", you could run it this way:
% ssh root@dory < uptime.aksh Pseudo-terminal will not be allocated because stdin is not a terminal. Password: up 2 days, 10 hours, 47 minutes, 48 seconds
The message about pseudo-terminal allocation is due to the ssh client; the issue that this message refers to can be dealt with by specifying the "-T" option to ssh.
In a context with dynamic children, it can be very useful to iterate over those children programmatically. This can be done by using the list function, which returns an array of dynamic children. For example, following is a script that iterates over every share in every project, printing out the amount of space consumed and space available:
script
run('shares');
projects = list();
for (i = 0; i < projects.length; i++) {
run('select ' + projects[i]);
shares = list();
for (j = 0; j < shares.length; j++) {
run('select ' + shares[j]);
printf("%s/%s %1.64g %1.64g\n", projects[i], shares[j],
get('space_data'), get('space_available'));
run('cd ..');
}
run('cd ..');
}
Here's the output of running the script, assuming it were saved to a file named "space.aksh":
% ssh root@koi < space.aksh Password: admin/accounts 18432 266617007104 admin/exports 18432 266617007104 admin/primary 18432 266617007104 admin/traffic 18432 266617007104 admin/workflow 18432 266617007104 aleventhal/hw_eng 18432 266617007104 bcantrill/analytx 1073964032 266617007104 bgregg/dashbd 18432 266617007104 bgregg/filesys01 26112 107374156288 bpijewski/access_ctrl 18432 266617007104 ...
If one would rather a "pretty printed" (though more difficult to handle programmatically) variant of this, one could directly parse the output of the get command:
script
run('shares');
projects = list();
printf('%-40s %-10s %-10s\n', 'SHARE', 'USED', 'AVAILABLE');
for (i = 0; i < projects.length; i++) {
run('select ' + projects[i]);
shares = list();
for (j = 0; j < shares.length; j++) {
run('select ' + shares[j]);
share = projects[i] + '/' + shares[j];
used = run('get space_data').split(/\s+/)[3];
avail = run('get space_available').split(/\s+/)[3];
printf('%-40s %-10s %-10s\n', share, used, avail);
run('cd ..');
}
run('cd ..');
}
And here's some of the output of running this new script, assuming it were named "prettyspace.aksh":
% ssh root@koi < prettyspace.aksh Password: SHARE USED AVAILABLE admin/accounts 18K 248G admin/exports 18K 248G admin/primary 18K 248G admin/traffic 18K 248G admin/workflow 18K 248G aleventhal/hw_eng 18K 248G bcantrill/analytx 1.00G 248G bgregg/dashbd 18K 248G bgregg/filesys01 25.5K 100G bpijewski/access_ctrl 18K 248G ...
Even in a context with static children, it can be useful to iterate over those children programmatically. This can be done by using the children function, which returns an array of static children. For example, here's a script that iterates over every service, printing out the status of the service:
configuration services
script
var svcs = children();
for (var i = 0; i < svcs.length; ++i) {
run(svcs[i]);
if (props().length !== 0)
printf("%-10s %s\n", svcs[i], get('<status>'));
run("done");
}
.
Here's the output of running the script, assuming it were saved to a file named "svcinfo.aksh":
% ssh root@koi < space.aksh Password: cifs disabled dns online ftp disabled http disabled identity online idmap online ipmp online iscsi online ldap disabled ndmp online nfs online nis online ntp online scrk online sftp disabled smtp online snmp disabled ssh online tags online vscan disabled
Reporting state on the system requires generating output. Scripts have several built-in functions made available to them to generate output:
|
When an error is generated, an exception is thrown. The exception is generally an object that contains the following members:
code - a numeric code associated with the error
message - a human-readable message associated with the error
Exceptions can be caught and handled, or they may be thrown out of the script environment. If a script environment has an uncaught exception, the CLI will display the details. For example:
dory:> script run('not a cmd')
error: uncaught error exception (code EAKSH_BADCMD) in script: invalid command
"not a cmd" (encountered while attempting to run command "not a cmd")
You could see more details about the exception by catching it and dumping it out:
dory:> script try { run('not a cmd') } catch (err) { dump(err); }
{
toString: <function>,
code: 10004,
message: 'invalid command "not a cmd" (encountered while attempting to
run command "not a cmd")'
}
This also allows you to have rich error handling, for example:
#!/usr/bin/ksh -p
ssh -T root@dory <<EOF
script
try {
run('shares select default select $1');
} catch (err) {
if (err.code == EAKSH_ENTITY_BADSELECT) {
printf('error: "$1" is not a share in the ' +
'default project\n');
exit(1);
}
throw (err);
}
printf('"default/$1": compression is %s\n', get('compression'));
exit(0);
EOF
If this script is named "share.ksh" and run with an invalid share name, a rich error message will be generated:
% ksh ./share.ksh bogus error: "bogus" is not a share in the default project
Whether using batched commands or scripting (or some combination), automated infrastructure requires automated access to the appliance. This should be done by creating users, giving them necessary authorizations, and uploading SSH keys.