文章
服务器与存储管理
作者:Peter Brouwer,2012 年 1 月
目录:
Oracle 的 Sun ZFS 存储设备通常使用浏览器用户界面 (BUI) 来管理,其高级图形用户界面具有独特的、易于使用的特性。您也可以使用命令行界面 (CLI),它在设备无法通过网络到达或者需要重复执行一组固定的命令时很有用。您可以使用设备的串行控制台或安全 shell (SSH) 访问 CLI。
|
与系统的命令行交互提供了一种途径来按固定顺序重复执行一些任务,就像一种批处理作业。当批处理功能要求有条件的执行代码时,必须使用脚本。CLI 命令可以在 CLI 中以交互方式输入,也可以通过文件传递到 CLI。
Sun ZFS 存储设备的脚本语言基于 JavaScript(ECMAScript 第 3 版),并带有一些扩展。JavaScript 是一种松散的解释型编程语言,可以提供面向对象的功能。脚本解释程序是设备 shell 的一部分,因此由 shell 解释和执行脚本。
工作流用于在设备中存储脚本,它们还为用户提供脚本访问管理以及参数验证功能。工作流包含脚本和版本信息,可以在 BUI 或 CLI 中启动。它们还可以通过警报或定时器事件来启动。
本文主要提供有关如何在 Sun ZFS 存储设备内使用 JavaScript 编程特性的详细信息。它还提供有关 Sun ZFS 存储设备脚本架构、JavaScript 与设备的接口以及用于执行脚本的方法的详细信息。
提供 JavaScript 语言或设备的 CLI 命令语言的教程则不在本文讨论范围内。熟悉 C、C++ 或诸如 Perl 或 Python 之类的编程语言将有助于您理解本文档中提供的 JavaScript 示例。
附录 A 包含参考资料的链接,其中包括有关 JavaScript 的详细信息。设备的联机帮助特性(可通过 BUI 访问)提供了有关 CLI 和 BUI 用法的详细信息。还请从 Oracle 统一存储系统文档库之一查看或下载一份 Sun ZFS Storage 7000 系统管理指南(在本文档中称为管理指南)。
注:本文还提供 PDF 文件形式。
可以通过以下三种方式使用 CLI 与 Sun ZFS 存储设备交互:
可以将固定顺序的 CLI 命令组织在一个文件中并发送到设备以便使用 SSH 执行。这是一种形式的命令批处理。命令执行时将使用登录设备的用户的权限。管理指南中详细描述了使用设备 shell 时可用的命令。
当您需要更多灵活性时(例如使用变量和用户定义的函数执行条件代码),可以在 CLI 中使用脚本。设备 shell 提供了脚本解释程序,它会执行以 JavaScript 编程语言编写的用户定义的脚本。脚本按照与批处理作业传递到设备相同的方式传递到设备,即通过 SSH 连接传入设备或在 CLI 提示符下以交互式方式传递。
工作流(可以封装脚本以便稍后执行)驻留在 Sun ZFS 存储设备中。您可以使用 BUI 或 CLI 启动这些工作流。定时器和警报事件也可用于触发启动工作流。您可以使用 SSH 连接或设备控制台访问设备。
设备 shell 提供了执行包含 JavaScript 编程代码的脚本的功能。因为 JavaScript 解释程序集成到设备 shell 中,所以它称作嵌入应用程序的环境。这不同于典型的 Web 浏览器的客户端-服务器 的 JavaScript 用法。JavaScript 库函数(如文档管理 (DOM))在嵌入应用程序的环境中不可用。参考 JavaScript 文档时,请使用 Core JavaScript Reference 找到有关可用 JavaScript 库函数的详细信息。
由于嵌入式 JavaScript 解释程序在执行期间解释代码,因此脚本在设备 shell 的上下文中执行,从设备或设备操纵检索的所有信息由将所需命令传递到 CLI 上下文的函数处理。这意味着同样的 CLI 导航命令可用于在 CLI 上下文结构中移动。例如,以下 CLI 命令导航到子上下文 net:
7000ppc1:> configuration net
7000ppc1:configuration net>
以下是等效的脚本命令:
run ('configuration net');
JavaScript 核心函数可从 JavaScript 对象库获取。该库包含操作复杂对象类型(如数组、字符串、数字对象的数学计算、正则表达式等)的函数(方法)。

图 1. 设备脚本架构
设备中存储的脚本受 Workflow Manager 的控制。Workflow Manager 通过使用 CLI 或 BUI 的用户或者通过定时器或警报事件控制脚本的启动。它还处理用户输入对话框、参数验证处理以及工作流中脚本执行的启动。
SSH 连接提供对 Sun ZFS 存储设备的 CLI 的访问。如清单 1 所示,show 命令提供有关 CLI 根上下文、可用属性和子上下文的信息。TAB 键列出当前上下文中的可用命令选项。
show 命令
pBrouwers MacBook-Pro:~ pBrouwer$ ssh root@192.168.56.101
Password:
Last login: Mon Feb 14 16:09:05 2011 from 192.168.56.1
7000ppc1:> show
Properties:
showcode = false
showstack = false
exitcoverage = false
showmessage = true
asserterrs = false
Children:
configuration => Perform configuration actions
maintenance => Perform maintenance actions
raw => Make raw XML-RPC calls
analytics => Manage appliance analytics
status => View appliance status
shares => Manage shares
7000ppc1:>
analytics coverage help script status
assert date ifconfig set time
assertlabels deny maintenance shares traceroute
configuration exit nslookup shell tree
confirm get ping show
cover getent raw sleep
7000ppc1:>
script 命令打开 JavaScript 解释程序以便您输入脚本语句。要执行,请键入句点 (.),然后按 Enter。
pBrouwers MacBook-Pro:~ pBrouwer$ ssh root@192.168.0.140
Password:
Last login: Mon Feb 14 16:09:05 2011 from 192.168.56.1
7000ppc1:> script
("." to run)> var i=10 ;
("." to run)> printf("i = %d\n",i);
("." to run)> .
i = 10
7000ppc1:>
您可以将脚本语句存储在一个文件中。然后可以通过 SSH 连接使用该文件内容向设备发送语句,如附录 B 所示。
要在设备上下文结构的层次结构中导航,请指定要导航到的上下文的名称,前面加上它之上的所有子级,从当前上下文开始。done 命令恢复先前的上下文环境。使用 UNIX cd 命令在层次结构中向上移动或返回根上下文:cd /。
以下是等效的脚本程序语句:
run('cd/');
某个子上下文中可用的命令可以通过指定到该子上下文的路径加上该命令直接从当前上下文执行。
7000ppc1:> configuration net interfaces list INTERFACE STATE CLASS LINKS ADDRS LABEL e1000g0 up ip e1000g0 192.168.56.101/24 i_e1000g0 e1000g1 up ip e1000g1 192.168.0.147/24 i_e1000g1 7000ppc1:>
如果无法从设备检索状态信息和操作其中的配置信息,则编写脚本没有任何用处。表 1 显示对可用 JavaScript 函数的扩展使您可以与设备交互。
表 1. JavaScript 函数的扩展| 函数 | 说明 |
|---|---|
run | 运行 shell 中的指定命令,以字符串形式返回任何输出。注意,如果输出包含多行,返回的字符串将包含嵌入的换行符。 |
props | 返回当前上下文的属性名称数组。 |
get | 获取指定属性的值。注意,此函数返回值的原生形式。例如,日期以 Date 对象的形式返回。 |
set | 接受两个字符串参数,将指定属性设置为指定值。 |
list | 返回与当前上下文的动态子级相对应的令牌数组。 |
这些命令是在设备的 CLI 上下文结构中执行的。有关详细信息,请参见管理指南第 1 章中的“脚本”。
Sun ZFS 存储设备的脚本功能是通过设备 CLI 层中内置的 JavaScript 语言解释程序实现的。所支持的 JavaScript 语法基于进行了一些扩展的 ECMA-3 标准。
JavaScript 名不符实,它其实与 Java 没有任何关系。JavaScript 是面向对象的编程语言,它与 C++ 和 Java 的区别在于它没有强类型检查。变量类型是在运行时定义的,而不是在编译过程中。因此,变量类型是动态的。
JavaScript 的名称中的术语Script(脚本)可能暗示它是一个简单的、过程类型的编程语言,但 JavaScript 实际上包含丰富的面向对象的特性集。熟悉面向对象的编程概念(例如 C++ 所用的那些编程概念)可以帮助您了解 JavaScript 的真正潜力。
对于简单任务,使用传统的过程类型编程特性就足以应付。对于更复杂的任务,需要 JavaScript 的面向对象特性,例如使用包含属性和操作对象属性的方法的对象。了解 JavaScript 的这些面向对象的方面(包括变量的作用域机制以及对象和数组的处理)对于处理更复杂的 JavaScript 编码至关重要。
本文档提供有关尝试将 JavaScript 用于更复杂任务时应注意的一些 JavaScript 概念的基本信息。附录 A 包含有关 JavaScript 的更多详细信息的资源清单。
注意,设备中使用的 JavaScript 环境在各种参考资料中被描述为嵌入应用程序的环境。但描述客户端 JavaScript 使用的资料不适用于 Sun ZFS 存储设备。请重点关注核心 JavaScript 部分。
JavaScript 语言的语法与 C 语言语法类似。它区分大小写,因此要求使用一致的变量和函数名称。分号用于终止语句,大括号 ({}) 用于分组语句块。虽然在 JavaScript 中使用分号在某些情况下是可选的,但最好始终在每条语句的结尾处使用分号以保持代码的易读性。可以识别 C 和 C++ 注释语法。
除了基本数据类型(如数字、布尔型和字符串)之外,JavaScript 还支持对象形式的复杂数据类型。基本数据类型的值会自动转换成操作所需的类型。
var index=1;
var textarray = new Array('Line 1','Line 2','Line 3');
var array_elements = textarray.length
for ( ; index < array_elements ; index++) {
printf('Array element' + index + ': ' + textarray[index] + '\n');
}函数是一种特殊类型的对象。这意味着函数可以存储在变量、数组和对象中。函数还可以作为参数传递到其他函数。
和其他语言中一样,JavaScript 使用 null 值指示变量不包含有效值。JavaScript 从不将某个值设为 null;这必须由程序员来显式执行。JavaScript 使用值 undefined 识别已声明但从未为其分配值的变量。在这种情况下,变量的类型未知。值 undefined 还用于标识对一个尚未存在的对象的属性的引用。
JavaScript 中的变量可以通过值或通过引用来引用。字符串是一个特例。在 JavaScript 中,字符串的内容是不可变的;它们永远不能更改。字符串只能通过创建一个新的字符串然后复制原始字符串中应保持不变的部分来更改。
请始终使用 var 语句来声明变量,因为它将变量的作用域固定在代码中的声明处。不使用 var 语句会自动使变量全局化,这可能会带来意想不到的副作用。例如,全局变量不会被的 JavaScript 垃圾收集机制清除,这可能会导致内存泄漏。
JavaScript 语言同时支持变量和属性。乍一看,它们似乎一样。二者都用于保存值。区别在于 JavaScript 解释程序在运行时创建它们的方式。基本上,属性属于对象,变量属于上下文。对于一般用户,没有实质区别。
在 JavaScript 语言中,属性用于添加有关对象的变量信息,例如,traffic-light.current-color=red。函数也被当作对象来处理,因此在函数中定义的变量被视为该函数的属性。
了解为属性赋值的不同方法和语法非常重要。有两种方法:值调用或引用调用;每种方法又有多种语法。为便于阅读和维护代码,可对保存对函数、对象和文本字符串的引用的变量使用“引用调用”的方法。
请在程序的顶部定义文本字符串,这样在维护代码时易于找到它们。
在示例 1 中,在代码的顶部定义了工作流结构的属性值。稍后您将注意到工作流结构必须在代码结尾处声明。示例 1 显示用于“引用调用”和“值调用”方法的语法。
示例 1:初始化workflow 对象属性的不同方法
/// File: Example 1
// Object initialized with two properties, using the object literal syntax,
// in which each property name/value pair is followed by a comma.
// The property name is followed by colon.
var MyTextObject = {
MyVersion: '1.0',
MyName: 'Example 1',
MyDescription: 'Example showing basic Object Workflow structure'
}
// New property added using variable assignment syntax.
MyTextObject.MyDescription = 'Use of properties example';
// Workflow object initialized using literal syntax and references.
// Values do not need to be literals; they can be references to other objects,
// as can be seen here.
var workflow = {
name: MyTextObject.MyName, // Reference syntax
description: MyTextObject.MyDescription, // Reference syntax
version: MyTextObject.MyVersion, // Reference syntax
origin: 'Oracle', // Literal syntax
execute: function () { return('Hello World'); } // Literal Syntax
JavaScript 使用其他语言中所有已为大家熟悉的运算符。此外,还使用了一种新型的运算符来测试“身份”:=== 和 !==。这些运算符与 == 和 != 运算符的不同之处在于它们不会造成像 == 和 != 运算符那样的自动类型转换。注意有关这些运算符的详细信息,因为差异有时很微妙。
var num = 10;
var num_string= '10';
if ( num==num_string )
print('True because values are the same\n');
if ( num===num_string)
print('Variables are of identical type and have the same value\n');
else
print('Variables do not have same value OR are not of identical type\n');
示例 1 中的代码将产生以下输出:
True because values are the same Variables do not have same value OR are not of identical type
注意所有算术运算符都可以与赋值运算符组合使用,例如:*=、+= 和 %=。这些语句读作 a 运算符 = b 和 a = a 运算符 b。
如果您熟悉 UNIX shell、C 或 C++ 编程,就可以轻松使用 JavaScript 编写程序。所有其他语言中大家所熟悉的语句,如 if then、while、for 和 switch case 语句,在 JavaScript 语言中都有。
一个很好的编程习惯是捕获在程序执行期间可能发生的运行时异常。异常事件由诸如函数失败或使用 run 命令 ('select myshare') 选择了一个并不存在的共享之类的事件触发。throw 语句强制发出显式异常信号。
捕获运行时异常并从异常恢复是用 JavaScript 表达式 try/catch/finally 语句来处理的。
示例 2 是一个简单的集群配置测试脚本,它检查当前设备节点是否是集群的一部分:
示例 2:简单的集群配置测试脚本
7000ppc1:> script
("." to run)> function ClusterTest(){
("." to run)> try {
("." to run)> run ('cd /');
("." to run)> run ('configuration cluster');
("." to run)> return(true);
("." to run)> }
("." to run)> catch (err) {
("." to run)> if (err=EAKSH_BADCMD) {
("." to run)> return(false);
("." to run)> }
("." to run)> else { // catch unknown condition
("." to run)> throw("Unexpected cluster test error");
("." to run)> }
("." to run)> }
("." to run)> }
("." to run)> // Main start
("." to run)> printf("Let's see if this node is part of a cluster; ");
("." to run)> if ( ClusterTest() )
("." to run)> printf("Yes it is\n");
("." to run)> else
("." to run)> printf("No it is not\n");
("." to run)> .
Let's see if this node is part of a cluster; No it is not
ClusterTest 函数使用 try/catch 构造来执行 run 命令以导航到子上下文集群。如果 run 命令失败,它将被 catch 语句捕获,在此语句中检查 run 命令是否失败。任何其他意外错误条件使用 throw 语句终止程序。
以下代码显示用于在 Sun ZFS 存储设备中加载和执行脚本的命令。记住,脚本被传递给设备 shell 中的脚本解释程序。
pBrouwers MacBook-Pro:~ pBrouwer$ ssh root@192.168.0.140
Password:
Last login: Mon Feb 14 16:09:05 2011 from 192.168.56.1
7000ppc1:> script
("." to run)> var i=10 ;
("." to run)> printf("i = %d\n",i);
("." to run)> .
i = 10
7000ppc1:>
在上述代码中,使用 script 命令激活脚本解释程序之后输入脚本语句。这些语句在键入句点 (.) 并按 Enter 键之后执行。此方法适合简单的交互式使用。
处理更复杂的脚本时,更轻松的办法是将命令组织到一个文本文件中,然后使用 SSH 连接将该文件发送到设备。此环境提供完整的 JavaScript 功能,如使用函数和条件语句。
示例 3 使用脚本的简化版本来创建和删除现有池和项目中的共享。
示例 3:创建和删除共享的简单脚本
// File: Example3.txt
script
// For ease of use, group all our arguments in one object.
// One could even use a shell script to create this JavaScript from a template
// using the arguments passed on to the user in a shell script.
// Version: 1.1.3
var MyArguments = {
pool: 'poola',
project: 'projecta',
share: 'volumeTest',
what: 'delete'
}
function CreateDeleteShare (Arg) {
run('cd /'); // Make sure we are at root child context level
run('shares');
try {
run('set pool=' + Arg.pool);
} catch (err) {
printf("Specified pool, %s not found\n ",Arg.pool);
return;
}
try {
run('select ' + Arg.project);
} catch (err) {
printf("Specified project, %s not found\n ",Arg.project);
return;
}
if (Arg.what=='create' ) {
try {
run('filesystem ' + Arg.share);
run('commit');
} catch(err) {
printf("Unable to create share, %s\n",Arg.share);
return;
}
printf('Successfully created share, '+Arg.share);
return;
} else {
try {
run('select' + Arg.share); // Check if share is there
run('done'); // Release for delete
run('confirm destroy ' + Arg.share);
} catch (err) {
if (err.code == 10004 )
printf("Specified share, %s, does not exist\n",Arg.share);
else printf("Unable to delete share, %s\n",Arg.share);
return;
}
printf("Successfully deleted share, %s\n", Arg.share);
}
}
// Kick off the create delete function using our object MyArguments to pass on the
// parameters needed for the job.
printf("About to %s share, %s from project, %s in pool %s\n",
MyArguments.what,
MyArguments.share,
MyArguments.project,
MyArguments.pool);
CreateDeleteShare(MyArguments);
该脚本使用对象 MyArguments 保存要使用的池、项目和共享的名称。此结构将所有变量元素置于脚本顶部,而不是将其硬编码在整个脚本中。运行时错误用 try/catch 构造来处理。为简单起见,对所捕获的错误类型未作区分。变量 err.code 可用于检查何种类型的错误导致 run 命令失败。
以此方式使用的脚本对于需要执行预定义任务的批处理类型环境是理想之选。当要求对脚本使用进行更严格的控制时,工作流是更好的选择。工作流中使用的脚本存储在设备中,一旦加载到设备中就不能修改,因此代码对最终用户不再可见。
还可以对工作流应用访问控制,例如,启用对为管理任务创建的工作流的限制性使用。工作流可以提示用户输入,下节中将对次进行介绍。
与使用工作流相比,除了用户输入提示,或通过设备定时器或警报事件启动触发器,使用 SSH 的脚本并没有提出其他限制。
以上所示示例使用 SSH 连接或在控制台 CLI 通过交互方式将脚本加载到设备中。要将脚本永久保存在设备中,必须将脚本置于 Workflow Manager 的控制之下。工作流使用登录设备所需的用户凭证在设备 shell 中异步执行。
为使 Workflow Manager 存储和执行脚本,脚本需要一些附加信息。Workflow Manager 使用这来将信息呈现给用户,并获取对工作流启动函数的访问权限。这些信息保存在名为 workflow 的对象中,该对象包含表 2 中所示的属性。
workflow 属性成员 | 必需的属性 | JavaScript 类型 | 说明 |
|---|---|---|
name | 字符串 | 工作流的名称 |
description | 字符串 | 工作流的简短描述 |
execute | 函数 | 要执行的脚本代码 |
| 可选属性 | JavaScript 类型 | 说明 |
version | 字符串 | 该工作流的版本,采用点分十进制(主.次.微)形式 |
required | 字符串 | 该工作流运行所需的最低设备软件版本 |
origin | 字符串 | 工作流提供商的名称 |
parameters | 对象 | 定义脚本输入参数的结构 |
validate | 函数 | 验证输入参数的 JavaScript 函数 |
创建 workflow 对象须遵循 JavaScript 对象文字语法:包含在大括号内的冒号分隔的属性/值对的逗号分隔列表。
在下面的代码中,<property> 是表 2 中所提到的属性名之一。
var workflow = {
<property>: <object literal>|<object reference>,
<property>: <object literal>|<object reference>,
};
<object literal> 中的值为字符串、函数或对象类型,如表 2 所述。
最小工作流脚本应如下所示:
// Example of basic workflow definition using object literals
// in the workflow object constructor.
var workflow = {
name: 'Minimum workflow code',
description: 'Example of basic workflow structure',
execute: function() { return('Hello World'); }
};
在 workflow 对象内部可以使用对先前定义的对象或变量的引用,而不是直接指定属性的数据值。下面的示例显示最小工作流定义。
// File: Example4.txt
// Example of basic workflow definition using variables
// in the workflow object's constructor
//
//
var WorkflowName = 'Minimum workflow code';
var WorkflowDescription = 'Example of basic workflow structure';
function Main () {
return('Hello World');
}
var workflow = {
name: WorkflowName,
description: WorkflowDescription,
execute: Main
};
要在设备中加载先前的工作流示例,可以在设备的“maintenance 工作流”子上下文中使用 BUI 或 CLI upload 命令。
以下屏幕截图显示 BUI 工作流的加载步骤。

图 2. BUI 工作流
使用 + 按钮将显示加载工作流文件对话框。

图 3. BUI,Add Workflow
加载过程中将检查工作流文件中的语法错误。
一旦加载,将按照 workflow 对象的 name 和 description 属性中的设置显示工作流的名称和描述信息。

图 4. BUI,新工作流已添加
要执行工作流,请双击它。

图 5. BUI,新工作流输出
使用工作流属性 parameters 向工作流添加一些输入参数(如清单 2 所示)可使工作流更有用。
parameters = {
<parameter1name>: {
label: <String>
type: <String>
}
<parameter2name>: {
label: <String>
type: <String>
options: <Array>
optionlabels: <Array>
}
<parameterNname>: {
label: <String>
type: <String>
optional: <Boolean>
}
};
这本身是一个具有以下结构的对象:
parameterNname 属性是输入参数的名称,在脚本代码中可通过此名称引用它。parameterNname 属性本身是一个对象,它总是需要包含标签和类型属性。label 属性的值。type 属性的值指定 paramenterNname 对象中存储的值的类型,如布尔值、字符串或文件。注:type 定义的完整列表可在管理指南中找到。
以下属性并非必需,但可以使用它们指定对参数的要求:
optional 属性 — 设置为 true 时,指定在 UI 中无需输入此参数。options-optionlabels 属性对 — 显示输入值的固定列表。要使用此函数,您必须将 type 属性设置为值 ChooseOne。接下来,您将看到 Workflow Manager 如何在工作流启动时使用对象中的属性。该示例显示了 BUI 界面。至于 CLI 交互,请参见管理指南。Workflow Manager 所使用的处理机制对于 BUI 和 CLI 都是一样的。
parameters 对象用于构建一个对话框,其中包含一些域,您可以在域中输入 parameters 对象所需的输入。您在域中填入数据并单击 PROCEED 后,Workflow Manager 将按照 validate 属性中指定的内容执行验证函数,并使用对 parameters 对象的引用作为参数。当 validate 函数指示有错误时,Workflow Manager 将再次显示该对话框,指示在哪个域中检测到错误。
一旦 validate 函数正常通过,Workflow Manager 会调用工作流属性 execute 中指定的函数,并且再次以对 parameters 对象的引用作为参数。
示例 5 采用创建/创建共享脚本的示例,对其进行了改编以用于工作流。Workflow Manager 提示输入发生共享创建/删除操作的池和项目的名称。删除/创建之间的选择通过一个下拉列表构造来呈现。
示例 5:创建和删除共享的基本工作流脚本
// Simple example of how to create/delete a share.
// File example5.txt
// Information to be used in workflow object:
var MyWorkflow = {
MyVersion: '1.0.0',
MyName: 'Create/Delete a share',
MyDescription: 'Example of how to create/delete a share',
Publisher: 'Oracle Corporation',
err: { // Definition of error codes.
// Define a range for your project that
// can be recognized by the sysadmins in
// your org.
WP_SCRIPT_WORKFLOWS_CREATE_SHARE: 8001,
WP_SCRIPT_WORKFLOWS_DELETE_SHARE: 8002,
}
}
// This example workflow uses four input parameters.
// The last parameter is an example of the use of a fixed list of values.
var MyParams = {
pool: { // Pool to create/delete the share.
label: 'Pool Name',
type: 'String',
},
project: { // Project to create/delete the share.
label: 'Project name',
type: 'String',
},
share: { // Share to create/delete.
label: 'Share name',
type: 'String',
},
what: { // Create or delete the share.
label: 'Operation',
type: 'ChooseOne',
options: ['create','delete'],
optionlabels: ['Create','Delete'],
}
}
// Verify function from workflow.
// Check whether pool and project exist.
function VerifyPoolandProject(p) {
var err_msg = ' does not exist';
run ('cd /'); // Make sure we are at root child context level.
run ('shares');
try { // Check whether pool name exists.
run('set pool='+p.pool);
} catch(err) {
return( {pool: 'Specified pool, ' + p.pool + err_msg } );
}
try {
run('select ' + p.project);
} catch(err) {
return( {project: 'Specified project, ' + p.project + err_msg } );
}
if (p.what=='delete' ) { // Check whether the share to be deleted exists.
try {
run('select'+ p.share);
}
catch(err) {
return({share: 'Specified share, ' + p.share + err_msg });
}
}
return;
}
function CreateDeleteShare (p) {
run('cd /'); // Make sure we are at root child context level.
run('shares');
run('set pool=' + p.pool);
run('select ' + p.project);
if (p.what=='create' ) {
try {
run('filesystem ' + p.share);
run('commit');
} catch(err) {
throw {
code: MyWorkflow.err.WP_SCRIPT_WORKFLOWS_CREATE_SHARE,
message: 'Unable to create ' +
p.share + ',' + err.message
}
}
return('Successfully created share, '+p.share);
} else {
try {
run('confirm destroy ' + p.share);
} catch(err) {
throw {
code: MyWorkflow.err.WP_SCRIPT_WORKFLOWS_DELETE_SHARE,
message: 'Unable to delete ' +
p.share + ',' + err.message
}
}
return('Successfully deleted share, '+p.share);
}
}
var workflow = {
name: MyWorkflow.MyName,
description: MyWorkflow.MyDescription,
version: MyWorkflow.MyVersion,
origin: MyWorkflow.Publisher,
parameters: MyParams,
validate: VerifyPoolandProject,
execute: CreateDeleteShare
};
注意,workflow 对象是在代码的结尾处指定的,因为必须先定义该对象中引用的所有对象和函数。为便于阅读和管理,所有脚本信息保存在一个对象中并在代码顶部指定。稍后将在 workflow 对象中使用对此对象元素的引用。在工作流中对 validate 和 execute 属性执行同样的操作。这使得工作流脚本更易于阅读。
MyParams 对象指定为此工作流从用户请求的输入信息。请求了四个参数:三个是文本类型,一个是含两项的列表,如图 6 所示。对 MyParams 对象的引用存储在 workflow 对象的 parameters 属性中。

图 6. 用户对话框示例
在 BUI 中执行该工作流将显示图 7 所示的对话框。
在本示例中,volumeqqq 不存在。单击 APPLY 将在 workflow 对象中的验证函数 ValidatePoolandProject 中触发共享选择错误,如图 7 所示。

图 7. 用户对话框输入错误示例
该错误是由以下语句导致的:
return({share: 'Specified share, ' + p.share + err_msg });
return 语句 share:包含对标志了错误的输入参数的属性名的引用。因此,Workflow Manager 会高亮显示触发错误的输入域值,并在错误消息之前显示 MyParams 对象中域定义的 label 属性的值。
通过 JavaScript try/catch 功能捕获错误。
还可以使用工作流实现一个自定义函数或操作以响应设备生成的警报。Sun ZFS 存储设备可以针对各种情况生成警报。警报消息存储在 Maintenance->Logs->Alerts 下。
通过定义一个警报操作,可以将工作流绑定到警报。由警报操作触发的工作流在后台运行,不会提示用户输入。管理指南的“配置”一节提供了有关警报、如何自定义警报以及可以在何处找到警报日志的详细信息。
为将工作流绑定到事件,必须向 workflow 对象添加新属性。alert 属性和 setid 属性都必须设置为 true 以便工作流使用工作流文件的所有者(如 owners 角色中的设置)的权限来运行。因为无需用户输入,所以不需要描述输入参数的对象。
示例 6 中的代码对上一示例进行了改编以提供一种非常简单的警报操作工作流。
示例 6:警报工作流的最少代码
// File: Example 6
// Example 5 adapted for alert usage
var MyTextObject = {
MyVersion: '1.0',
MyName: 'Example 6',
MyDescription: 'Example of use of Alert',
Origin: 'Oracle'
};
var workflow = {
name: MyTextObject.MyName,
description: MyTextObject.MyDescription,
version: MyTextObject.MyVersion,
alert: true, // Workflow triggered by alert
setid: true,
origin: MyTextObject.Origin,
execute: function (MyAlert) {
audit('workflow started for alert'+MyAlert.uuid);
}
};
为使工作流向外界发送信息,必须使用 audit 函数。此函数接受一个字符串作为参数,文本放在应用程序审计日志中。Workflow Manager 将一个对象传递到为 execute 属性定义的函数。此对象包含表 3 所示的元素。
execute 属性中的元素 | 属性 | 类型 | 说明 |
|---|---|---|
class | 字符串 | 警报的类 |
code | 字符串 | 警报的代码 |
uuid | 字符串 | 警报的唯一标识符 |
timestamp | 日期 | 事件时间 |
items | 对象 | 包含有关事件详细信息的对象 |
items 属性是一个对象,包含有关触发工作流的事件的以下详细信息。
| 属性 | 类型 | 说明 |
|---|---|---|
url | 字符串 | 指向包含事件描述的 Web 页面的 URL |
action | 字符串 | 用户为了响应事件应采取的操作 |
impact | 字符串 | 促成警报的事件的影响 |
description | 字符串 | 易读的描述警报的字符串 |
severity | 字符串 | 促成警报的事件的严重性 |
response | 字符串 | 自动响应操作信息 |
type | 字符串 | 警报类型,例如,minor 或 major |
这些信息还显示在 BUI 中,以响应对有关警报的详细信息的请求,如图 8 所示。

图 8. BUI,详细的警报信息
例如,为将示例代码绑定到设备中的一个复制事件,必须先将工作流加载到设备中。图 9 中的屏幕截图显示了您必须在配置上下文中定义警报操作的对话框。警报操作必须绑定到某个事件类别。对于每个类别,您可以选择一个子集事件类型。
您可以配置多个警报操作。如果选择“Execute workflow”,将执行先前加载的示例脚本。
在本示例中,设置了两个将在复制操作失败时执行的操作:发送电子邮件和启动工作流。注意手动触发所配置操作的 TEST 选项。

图 9. BUI,添加警报操作
本节介绍编码风格、JavaScript 和培训的最佳实践。
由于 JavaScript 的编程结构与编程语言 C 和 C++ 的结构类似,因此 C 或 C++ 的代码编写标准实践适用于 JavaScript 代码,例如,大括号和缩进的使用,如本文档中所见。
请使用有意义、易于理解的变量名。不要使用诸如 i、n 或 x 之类的变量。保持变量名短小、简明,但带描述性。例如,FC_Lun 或 iSCSI_Lun 变量名比 LUN 更有意义。
使用编程语言时,请使用英语。这样不但便于共享工作流脚本,而且便于讲非本地语言的同事请求输入或支持。
请使用简单、有条理的代码语句,并添加注释。注释应添加信息,而不是重复已有的信息。写得好的注释应有助于您在一年之后回到一段代码进行更新。当工作流准备共享时,从嵌入的注释应可清晰了解其用法和目的。
请遵循以下 JavaScript 最佳实践:
MyWorkflow 对象。使用对象还有助于创建易于维护的可移植代码。var 语句定义变量。如果变量尚未定义,JavaScript 在使用该变量时将视其为使用全局作用域进行过定义。因此,脚本可能会不断增加内存使用率,因为未定义(现为全局)变量不包括在 JavaScript 垃圾回收机制内。这种现象称为内存泄漏。with 语句。with 语句经常用于在处理深度嵌套对象层次结构时省却键入。但对于 JavaScript 遍历层次结构链时如何解析变量却没有任何控制。对于要在 with 语句中使用的对象,请使用一个变量来保存对该对象的引用。以下是建议的培训最佳实践:
本节提供一些简单示例,它们可能促使您以完美的方式解决问题。但这些示例本身并非完善的解决方案。为简单起见,以下代码示例并不处理错误检查和异常。
通过在设备上安装一个公钥,避免每次使用 SSH 执行 CLI 脚本时提示输入口令,公钥是在执行脚本的主机上使用 ssh-keygen -t rsa -b 1024 命令生成的。可以使用 ssh 的以下语法执行安装脚本,其中 <key_rsa> 是包含使用 ssh-keygen 命令生成的密钥的文件的名称。
ssh -i .ssh/<key_rsa> root@MyAppliance
使用 SSH 连接进行访问时向设备发送命令序列有时可能会令人困惑。要了解可用选项,请回头参考基础知识。和任何 UNIX 类型的命令界面一样,SSH 有两种 I/O 机制:stdin 和 stdout。基本上,stdin 是 SSH 的输入通信接口,stdout 是 SSH 的输出通信接口。输入和输出可以重定向。
ssh root@myAppliance < inputfile > outputfile
在上面的命令中,输入被重定向以便 ssh 从 inputfile 文件读取字符,来自 ssh 的输出被重定向到 outputfile 文件。因此如果 inputfile 如以下所示,inputfile 中的字符将被发送到设备的 CLI。
configuration net interfaces list
该命令序列的输出被 ssh 拾取并重定向到 outputfile。
在设备的 CLI 提示符下,您可以交互地输入 JavaScript 命令并执行。这意味着您可以将 JavaScript 命令写入 inputfile 文件,使用 ssh 将其发送到设备,然后在 outputfile 中捕获 JavaScript 命令的输出。这非常强大,因为它意味着您可以编写“智能”批处理作业。此功能将批处理命令环境与智能动态编码环境相融合。
了解了 ssh 机制之后,现在您可以编写脚本,使用 ssh 将其发送到设备,然后可以在客户端检查脚本的退出状态。
因此,举例来说,如果您有一个创建共享的脚本,您想检查该脚本的一次失败,可以将错误代码返回到 SSH(客户端 shell)并在客户端 shell 代码中对错误情况进行响应。
示例 7 使用示例 3 中旧的创建/删除共享脚本。首先,您必须使用返回数字代码的代码替换所有发出文本消息的代码。文本字符串在 shell 中太难处理。然后,您必须添加额外的 shell 编码来处理 shell 参数处理并启动要使用的 shell 变量,以便将这些变量传递到设备脚本部分。
注意,文件中两个“EOF”字符串之间的文本是要使用 ssh stdin 发送到设备的 JavaScript 代码部分。来自 JavaScript 部分的错误代码用 JavaScript 代码结尾处的 print 命令传递回来,并捕获到 shell 脚本的 ScriptError 变量中。
#!/bin/sh
# File example7.txt
# Shell script using input arguments to be passed to appliance script job.
# Script error codes are passed back to the shell.
Usage() {
echo "$1 -u <Appliance user> -h <appliance> -s <share> -c|-d -j <project> -p <pool>"
exit 1
}
# Error code definitions
PoolNotFound=100
ProjectNotFound=101
CreateShareFailed=102
DeleteShareFailed=103
UnKnownError=999
#
# Shell script main
PROG=$0
# Check used command line options
while getopts u:h:s:j:p:cd flag
do
case "$flag" in
c) create="true"; action="create";;
d) delete="true"; action="delete";;
p) pool="$OPTARG";;
j) project="$OPTARG";;
s) share="$OPTARG";;
u) user="$OPTARG";;
h) appliance="$OPTARG";;
\?) Usage $PROG ;;
esac
done
# Create and Delete action are multi-exclusive
[ "$create" = "true" -a "$delete" = "true" ] && Usage $PROG
# None of the arguments can be empty
[ -z "$pool" -o -z "$project" -o -z "$share" -o -z "$appliance" -o -z "$user" ] && Usage $PROG
#
# Now get to the job at hand
# Start ssh and feed the script code in using stdin
ScriptError=`ssh $user@$appliance << EOF
script
// Above command activates script mode in the appliance.
// For ease of use, group all arguments in one object.
// Note we use the shell variables obtained from the shell command line.
// Version: 1.0.0
var MyArguments = {
pool: '$pool',
project: '$project',
share: '$share',
what: '$action'
}
// We could use the script variables throughout the scripting code but it might create
// confusion, so keep all the shell-script variable interaction concentrated in one
// place in the script.
var MyErrors = {
PoolNotFound: '$PoolNotFound',
ProjectNotFound: '$ProjectNotFound',
CreateShareFailed: '$CreateShareFailed',
DeleteShareFailed: '$DeleteShareFailed',
UnKnownError: '$UnknownError',
}
function CreateDeleteShare (Arg) {
run('cd /'); // Make sure we are at root child context level
run('shares');
try {
run('set pool=' + Arg.pool);
} catch (err) {
return(MyErrors.PoolNotFound);
}
try {
run('select ' + Arg.project);
} catch (err) {
return(MyErrors.ProjectNotFound);
}
if (Arg.what=='create' ) {
try {
run('filesystem ' + Arg.share);
run('commit');
} catch(err) {
return(MyErrors.CreateShareFailed);
}
return(0);
} else {
try {
run('select' + Arg.share); // Check if share is there
run('done'); // Release for delete
run('confirm destroy ' + Arg.share);
} catch (err) {
if (err.code == 10004 )
return(MyErrors.DeleteShareFailed);
else return(MyErrors.UnKnownError);
}
return(0);
}
}
// Kick off the create delete function using our MyArguments object to pass on the
// parameters needed for the job.
err=CreateDeleteShare(MyArguments);
// The devil is in the tail; return the error code to stdout of ssh so the shell can
// pick it up in the ScriptError variable.
print(err);
.
EOF`
echo $ScriptError
[ "$ScriptError" != "0" ] && {
case $ScriptError in
$PoolNotFound) Message="Specified pool : $pool, not found";;
$ProjectNotFound) Message="Specified project : $project, not found";;
$CreateShareFailed) Message="Share $share could not be created";;
$DeleteShareFailed) Message="Share $share could not be deleted";;
$UnknownError) Message="Unexpected script error";;
esac
echo $Message
exit 1
}
echo "$action of share: share in project: $project, pool: $pool, was successful"
注意,shell 变量可以在整个 JavaScript 部分使用,但这会使 JavaScript 更难以维护。为便于使用,所使用的所有 shell 变量集中在 JavaScript 部分的开头。还要注意,在 CLI 脚本被发送到设备之前,shell 将替换脚本中的 shell 变量。
Sun ZFS 存储设备模拟器是一个出色的工具,可以让您熟悉设备 CLI 接口和脚本语言。请使用设备控制台或 SSH 连接访问 CLI。

图 10. 使用模拟器访问设备控制台
您可以通过交互式方式输入脚本命令,来了解 JavaScript 语言在设备中的语法和用法。
您可以使用通过控制台获得的网络接口配置信息启动与设备的 SSH 连接。使用 SSH 可使得在设备中执行一批 CLI 命令或一些简单脚本变得更轻松。

图 11. 与设备的 SSH 连接
命令可以组织到一个文件中。通过将该文件提供给 ssh 命令的 stdin 选项,可以将一批命令输入设备。
例如,将以下命令放入 mybatchjob 文件中:
Configuration net interfaces list
script printf("hello\n") ; printf("End of My Batch Job\n");
然后将该文件提供给 ssh 命令:
pBrouwers MacBook-Pro:~ pBrouwer$ ssh root@192.168.56.101 < mybatchjob Password: Last login: Mon Feb 14 16:09:05 2011 from 192.168.56.1 INTERFACE STATE CLASS LINKS ADDRESS LABEL E1000g0 up ip e1000g0 192.168.56.101 i_e1000g0 Hello End of My Batch Job pBrouwers MacBook-Pro:~ pBrouwer$
| 修订版 1.0,2012 年 1 月 31 日 |