This tutorial is for NodeBrain (version 0.7.4, 3 February 2009), an open source agent for state and event monitoring.
Copyright © 2006, 2007, 2008, 2009 The Boeing Company
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section titled GNU Free Documentation License preceding the index at the end of this document.
Rule Engine Tutorials
Node Module Tutorials
References
This tutorial illustrates how to invoke NodeBrain and introduces the reader to modes of operation that enable NodeBrain to adapt in minor ways to the requirements of different components within a monitoring application. Some basic concepts about the language syntax and semantics are introduced along the way.
The easiest way to get started with NodeBrain is to invoke it interactively, type some commands and watch it respond. This tutorial will walk you through some simple interactive sessions.
If you execute NodeBrain with no arguments, it starts in interactive mode and presents a prompt. At a Unix or Linux shell command prompt you start NodeBrain by typing nb. It looks like this.
$ nb
N o d e B r a i n 0.7.4 (Dunce) 2008/11/15
Compiled Jan 28 2009 06:14:15 s390x-ibm-linux-gnu
Copyright (C) 1998-2009 The Boeing Company
GNU General Public License
----------------------------------------------------------------
nb
Date Time Message
---------- -------- --------------------------------------------
2009/01/28 06:14:21 NB000I NodeBrain nb[28964] myuser@myhost
2009/01/28 06:14:21 NB000I User profile /home/myuser/.nb/user.nb loaded.
2009/01/28 06:14:21 NB000I Reading from standard input.
---------- --------
>
Since the information NodeBrain displays when starting up is not important to the topics covered in this tutorial, we will not include it in our examples. The example above is reduced to this.
$ nb
>
At a Windows command prompt it looks like this.
C:> nb
>
For the examples in this tutorial we will use the Unix style shell command prompt.
To end an interactive session, just tell NodeBrain you want to quit.
$ nb
> quit
$
You may also use Cntl-D or Cntl-C to end an interactive session.
The partisan, when he is engaged in a dispute, cares nothing about the rights of the question, but is anxious only to convince his hearers of his own assertions. – Plato (427 BC - 347BC), Dialogues, Phaedo
It is quite easy to convince NodeBrain of your own assertions. It looks like this.
$ nb
> assert a=1,b=2,mood="happy";
To verify that NodeBrain is willing to accept your assertions, ask it to show you what it now believes about a, b and mood;
> show a
a = 1
> show b
b = 2
> show mood
mood = "happy"
> quit
Hell, there are no rules here–we're trying to accomplish something. — Thomas A. Edison (1847 - 1931)
If you are trying to accomplish something with NodeBrain and there are no rules, you are using the wrong tool. NodeBrain is a rule engine, not a general purpose shell or scripting language. There are many good shells and scripting languages to choose from, and much to be accomplished without rules — at least not the kind of rules we are talking about here. In a more general sense, no computing exists without rules, but the kind of rules you find in NodeBrain and similar tools are not appropriate for most computing problems. However, they can be quite handy when applied to the right problems.
Let's start with a rule that doesn't accomplish anything.
$ nb
> define myFirstRule on(a=1 and b=2);
> assert a=1;
> assert c=3;
> assert b=2;
2008/06/05 12:19:05 NB000I Rule myFirstRule fired
> assert a=0;
> assert c=1;
> assert a=1;
2008/06/05 12:19:21 NB000I Rule myFirstRule fired
> quit
$
What just happened there? Well, we defined a rule named myFirstRule and specified a condition (a=1 and b=2) under which we want it to fire. Each time this condition transitioned to a "true" state, the rule fired.
Let's modify this example to make it accomplish something—perhaps an assertion.
$ nb
> define mySecondRule on(a=1 and b=2) mood="happy";
> assert mood="sad";
> show mood
mood = "sad"
> assert a=1,b=2,c=3;
2008/06/05 12:20:48 NB000I Rule mySecondRule fired(mood="happy")
> show mood
mood = "happy"
> quit
$
The assertion that a=1, b=2 and c=3 changed our mood from "sad" to "happy". Who said you can't accomplish something by following rules?
She did not talk to people as if they were strange hard shells she had to crack open to get inside. She talked as if she were already in the shell. In their very shell. — Marita Bonner
From NodeBrain you can get into the host systems command shell by using a command prefix of -.
$ nb
> -date
[17445] Started: -date
[17445| Thu Jun 512:22:34 PDT 2008
[17445] Exit(0)
> -ls -al kim.nb
[17478] Started: -ls -al kim.nb
[17478| -rw-r--r-- 1 myuser users 52 2006-01-05 11:44 kim.nb
[17478] Exit(0)
> quit
$
When you talk as if you are in someone else's shell, you need to speak their language. Here's an example on Windows.
C:> nb
> -dir
[1060] Started: -dir
[1060| Volume in drive C has no label.
[1060| Volume Serial Number is 0C62-77CF
[1060|
[1060| Directory of C:\home\nodebrain\nb-0.6.4-source.msdsw\Release
[1060|
[1060| 03/28/2006 05:23 PM <DIR> .
[1060| 03/28/2006 05:23 PM <DIR> ..
[1060| 03/28/2006 04:26 PM 303,104 nb.exe
[1060| 03/28/2006 04:26 PM 10,163 nb.exp
[1060| 03/28/2006 04:26 PM 17,728 nb.lib
[1060| 3 File(s) 2,942,497 bytes
[1060| 2 Dir(s) 11,946,586,112 bytes free
[1060] Exit(0)
> quit
$
When we direct NodeBrain to execute another command to perform a task, we refer to the child process as servant.
CONSULT,v.i. To seek another's disapproval of a course already decided on. — Ambrose Bierce (1842 - 1914), The Devil's Dictionary
NodeBrain is willing to accept the advice of others—even servants. To illustrate this, let’s first write a simple Perl script named processCount.pl that checks to see how many processes are running on a Unix system.
#!/usr/bin/perl
chomp($processes=`ps -e|wc -l`);
print("assert processes=$processes;\n");
Now let's execute it in our Unix command shell.
$ ./processCount.pl
assert processes=40;
Now we can consult this script from NodeBrain by using a prefix of "-:" instead of just "-". This tells NodeBrain to accept the output of the shell command as input to the NodeBrain interpreter.
$ nb
> -:./processCount.pl
[17537] Started: -:./processCount.pl
[17537: assert processes=40;
[17537] Exit(0)
> show processes
processes = 40
> quit
$
You ask me why I do not write something... I think one's feelings waste themselves in words, they ought all to be distilled into actions and into actions which bring results. - Florence Nightingale (1820 -1910), in Cecil Woodham-Smith, Florence Nightingale, 1951
Any NodeBrain command can be used as a rule action by following the rule condition with a colon : and the action command.
$ nb
> define cmdRule on(a and b): show x;
> assert x=3;
> assert a,b;
2008/06/05 12:27:41 NB000I Rule cmdRule fired
: show x;
x = 3
> quit
$
When the action you want to take in response to a condition is not a trivial command, you can often turn it into one by writing a script in your favorite scripting language. We'll illustrate this in an odd way by writing a trivial Perl script and ask you to imagine that it isn't trivial. We'll call this script alarmMe.pl.
alarmMe.pl
#!/usr/bin/perl
$id=shift();
$msg=shift();
$me=getpwuid($<);
system("mailx -s \"$id: $msg\" $me < note$id.txt");
Here's a session that uses a rule that invokes our alarmMe.pl script as a servant.
$ nb
> define alarmRule on(a):-alarmMe.pl 1 "Things aren't looking good"
> assert a;
2008/06/05 12:31:42 NB000I Rule alarmRule fired
: -alarmMe.pl 1 "Things aren't looking good"
[17571] Started: -alarmMe.pl 1 "Things aren't looking good"
[17571] Exit(0)
> quit
$
A rule file is just a file that contains NodeBrain rules. By convention we use a .nb suffix on these files. We can combine what we've learned in this section into a rule file named roger.nb.
# roger.nb
define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes"
define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes"
define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
When we specify a file as an argument to NodeBrain, it reads the file and processes the commands.
$ nb roger.nb
2008/06/05 14:37:14 NB000I Argument [1] roger.nb
> # roger.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
2008/06/05 14:37:14 NB000I Source file "roger.nb" included. size=246
2008/06/05 14:37:14 NB000I NodeBrain nb[5393] terminating - exit code=0
$
But wait, this is a tutorial on interactive sessions. What happened to our prompt? Remember we said that NodeBrain assumes we want an interactive session when we don't specify any arguments? Well, when we specify a file NodeBrain assumes we want a batch session and just processes the file and quits.
To tell NodeBrain we want an interactive session after the file is processed, we need to give it an additional - argument.
$ nb roger.nb -
2008/06/05 14:40:32 NB000I Argument [1] roger.nb
> # roger.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
2008/06/05 14:40:32 NB000I Source file "roger.nb" included. size=246
2008/06/05 14:40:32 NB000I Argument [2] -
2008/06/05 14:40:32 NB000I Reading from standard input.
---------- --------
>
Except for the user prompt, everything you learned in the previous section on interactive sessions applies to batch sessions. The rules are the same; we just no longer assume a user is typing commands.
In the previous section we actually ran a batch session by accident.
$ nb roger.nb
2008/06/05 14:42:37 NB000I Argument [1] roger.nb
> # roger.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
2008/06/05 14:42:37 NB000I Source file "roger.nb" included. size=246
2008/06/05 14:42:37 NB000I NodeBrain nb[5409] terminating - exit code=0
$
We start a batch session by giving nb at least one source file without specifying the "-" option. A batch session ends when all source files have been processed.
Let's create a second source file with some assertions; call it abby.nb.
# abby.nb
assert a=1,b=3;
assert b=2;
assert a=0;
assert a=1;
Let's combine the rule file and assertions in a single batch session.
$ nb roger.nb abby.nb
2008/06/05 20:35:59 NB000I Argument [1] roger.nb
> # roger.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
2008/06/05 20:35:59 NB000I Source file "roger.nb" included. size=251
2008/06/05 20:35:59 NB000I Argument [2] abby.nb
> # abby.nb
> assert a=1,b=3;
> assert b=2;
2008/06/05 20:35:59 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[7141] Started: -:./processCount.pl
[7141: assert processes=58;
2008/06/05 20:35:59 NB000I Rule myProcessRule2 fired
: -./alarmMe.pl 2 "ok - under 100 processes."
[7145] Started: -./alarmMe.pl 2 "ok - under 100 processes."
[7145] Exit(0)
[7141] Exit(0)
> assert a=0;
> assert a=1;
2008/06/05 20:36:00 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[7152] Started: -:./processCount.pl
[7152: assertprocesses=62;
[7152] Exit(0)
2008/06/05 20:36:00 NB000I Source file "abby.nb" included. size=52
2008/06/05 20:36:00 NB000I NodeBrain nb[7140] terminating - exit code=0
$
This time our batch session generated an alarm saying "ok - under 100 processes". Do you know why rule myProcessRule3 fired twice, but rule myProcessRule2 only fired once? It is because the rule condition for myProcessRule3 transitioned to a true state twice, while the condition for myProcessRule2 only transitioned to a true state once. An on rule whose condition is true will not fire when a new assertion causes it to remain true.
If you give NodeBrain an equal symbol = as an argument, it processes stdin like any other file. Let's do the same thing we did in the previous example using a pipe to stdin.
$ cat roger.nb abby.nb | nb =
We can combine the = argument with other source file arguments.
$ cat abby.nb | nb roger.nb =
Files are processed in the order of arguments. If we change the argument order the assertions are made before we define the rules, so we don't get the same results.
$ cat abby.nb | nb = roger.nb
This is like specifying the following command.
$ nb abby.nb roger.nb
Ok, so we now know how to pipe commands into NodeBrain. Maybe it would be handy to use a script as the source of assertions. Here's a script called randy.pl that will generate some pseudo random values for a and b.
randy.pl
#!/usr/bin/perl
for($i=1;$i<10;$i++){
$r=int(rand()*4);
if($r%2){print("assert a=$r;\n");}
else{print("assert b=$r;\n");}
}
We can use this script as the source of our assertions by piping the output of the script to NodeBrain.
$ ./randy.pl | nb roger.nb =
2008/06/05 20:54:23 NB000I Argument [1] roger.nb
> # roger.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
2008/06/05 20:54:23 NB000I Source file "roger.nb" included. size=251
2008/06/05 20:54:23 NB000I Argument [2] =
2008/06/05 20:54:23 NB000I Reading from standard input.
---------- --------
| assert a=3;
| assert a=1;
| assert a=1;
| assert b=0;
| assert b=2;
2008/06/05 20:54:23 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[7251] Started: -:./processCount.pl
[7251: assert processes=58;
2008/06/05 20:54:23 NB000I Rule myProcessRule2 fired
: -./alarmMe.pl 2 "ok - under 100 processes."
[7255] Started: -./alarmMe.pl 2 "ok - under 100 processes."
[7255] Exit(0)
[7251] Exit(0)
| assert b=0;
| assert b=2;
2008/06/05 20:54:24 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[7262] Started: -:./processCount.pl
[7262: assert processes=62;
[7262] Exit(0)
| assert a=1;
| assert b=0;
2008/06/05 20:54:25 NB000I NodeBrain nb[7250] terminating - exit code=0
$
We can also pipe from within a script. In randal.pl we open a pipe to NodeBrain and write to the pipe.
randal.pl
#!/usr/bin/perl
open(NB,"|nb roger.nb =")||die;
for($i=1;$i<10;$i++){
$r=int(rand()*4);
if($r%2){print(NB "assert a=$r;\n");}
else{print(NB "assert b=$r;\n");}
}
close(NB);
The result is the same.
$ ./randal.pl
2008/06/05 21:02:43 NB000I Argument [1] roger.nb
> # roger.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
2008/06/05 21:02:43 NB000I Source file "roger.nb" included. size=251
2008/06/05 21:02:43 NB000I Argument [2] =
2008/06/05 21:02:43 NB000I Reading from standard input.
---------- --------
| assert b=0;
| assert b=0;
| assert a=1;
| assert b=2;
2008/06/05 21:02:43 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[7376] Started: -:./processCount.pl
[7376: assert processes=59;
2008/06/05 21:02:44 NB000I Rule myProcessRule2 fired
: -./alarmMe.pl 2 "ok - under 100 processes."
[7380] Started: -./alarmMe.pl 2 "ok - under 100 processes."
[7380] Exit(0)
[7376] Exit(0)
| assert a=3;
| assert b=2;
| assert b=0;
| assert a=3;
| assert a=1;
2008/06/05 21:02:44 NB000I NodeBrain nb[7375] terminating - exit code=0
$
So, any way you can establish a pipe to NodeBrain is fine, just remember to tell NodeBrain to read stdin by specifying = as a file argument.
If thou are a master, be sometimes blind; if a servant, sometimes deaf. – Thomas Fuller (1608 -1661)
When NodeBrain reads commands from stdin in batch mode, it uses blocking I/O. This means it does nothing while waiting for the next command from the master. If the master is mute, NodeBrain is an idle slave. In servant mode, NodeBrain reads stdin using non-blocking I/O and is free to perform other duties while waiting for the next command from the master.
Let's first learn a simple way to give NodeBrain something else to do while waiting for commands. Here's a rule file like roger.nb that includes a scheduling rule. We'll call it jeeves.nb.
# jeeves.nb
define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
define mySchedulingRule on(~(3s)) a=0;
The rule condition ~(3s) specifies that the rule should fire every 3 seconds. When the mySchedulingRule fires, it asserts that a=0. If NodeBrain is waiting on input from stdin, it can not trigger this rule as specified. So we're going to have to change the way we run NodeBrain.
Here's a script called bertie.pl that uses the -s option to put NodeBrain in servant mode once all the input files specified as arguments have been processed.
bertie.pl
#!/usr/bin/perl
use FileHandle;
open(NB,"|nb jeeves.nb -s")||die; # use servant option
NB->autoflush(1); # force output to nb as soon as we send a command
for($i=1;$i<10;$i++){
$r=int(rand()*4);
if($r%2){print(NB "assert a=$r;\n");}
else{print(NB "assert b=$r;\n");}
sleep(5); # pretend like we are busy working on something
}
print(NB "stop\n"); # stop the servant mode nb
close(NB);
You can see from the output below that the activity scheduled by mySchedulingRule is taking place between commands sent by bertie.pl.
$ ./bertie.pl
2008/06/05 21:31:00 NB000I Argument [1] jeeves.nb
> # jeeves.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> define myProcessRule3 on(a=1 and b=2):-:./processCount.pl
> define mySchedulingRule on(~(3s)) a=0;
2008/06/05 21:31:00 NB000I Source file "jeeves.nb" included. size=283
2008/06/05 21:31:00 NB000I Argument [2] -s
2008/06/05 21:31:00 NB000T Servant mode selected
---------- ----------------------------------------------------
> assert a=3;
2008/06/05 21:31:03 NB000I Rule mySchedulingRule fired (a=0)
> assert b=0;
2008/06/05 21:31:06 NB000I Rule mySchedulingRule fired (a=0)
2008/06/05 21:31:09 NB000I Rule mySchedulingRule fired (a=0)
> assert a=3;
2008/06/05 21:31:12 NB000I Rule mySchedulingRule fired (a=0)
2008/06/05 21:31:15 NB000I Rule mySchedulingRule fired (a=0)
> assert b=2;
2008/06/05 21:31:18 NB000I Rule mySchedulingRule fired (a=0)
> assert b=0;
2008/06/05 21:31:21 NB000I Rule mySchedulingRule fired (a=0)
2008/06/05 21:31:24 NB000I Rule mySchedulingRule fired (a=0)
> assert a=3;
2008/06/05 21:31:27 NB000I Rule mySchedulingRule fired (a=0)
2008/06/05 21:31:30 NB000I Rule mySchedulingRule fired (a=0)
> assert b=2;
2008/06/05 21:31:33 NB000I Rule mySchedulingRule fired (a=0)
> assert b=2;
2008/06/05 21:31:36 NB000I Rule mySchedulingRule fired (a=0)
2008/06/05 21:31:39 NB000I Rule mySchedulingRule fired (a=0)
> assert a=1;
2008/06/05 21:31:40 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[7537] Started: -:./processCount.pl
[7537: assert processes=59;
2008/06/05 21:31:40 NB000I Rule myProcessRule2 fired
: -./alarmMe.pl 2 "ok - under 100 processes."
[7541] Started: -./alarmMe.pl 2 "ok - under 100 processes."
[7541] Exit(0)
[7537] Exit(0)
2008/06/05 21:31:43 NB000I Rule mySchedulingRulefired (a=0)
2008/06/05 21:31:45 NB000I Rule mySchedulingRulefired (a=0)
> stop
2008/06/05 21:31:45 NB000I NodeBrain nb[7536] terminating - exit code=0
$
Monitoring applications normally require a program that runs constantly and mostly without user control, but acts on the user's behalf. These qualities of persistence, relative autonomy, and user directed goals are characteristic of a software agent. We know these programs as daemons on UNIX or Linux, and services on Windows.
Let's modify our rules from the previous example so we execute our process counting script every minute.
# maxwell.nb
define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
define myProcessRule3 on(~(m)):-:./processCount.pl
Now we'll execute it.
$ nb maxwell.nb
2008/06/05 21:45:53 NB000I Argument [1] maxwell.nb
> # maxwell.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> # check process count every minute
> define myProcessRule3 on(~(m)):-:./processCount.pl
2008/06/05 21:45:53 NB000I Source file "maxwell.nb" included. size=273
2008/06/05 21:45:53 NB000I NodeBrain nb[7618] terminating - exit code=0
$
But wait, that’s not right. The program just ended after loading the rules. We need a way to tell it to run as a daemon. We’ll try the daemon option, “-d”.
$ nb –d maxwell.nb
2008/06/05 21:47:28 NB000I Argument [1] -d
2008/06/05 21:47:28 NB000I Argument [2] maxwell.nb
> # maxwell.nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> # check process count every minute
> define myProcessRule3 on(~(m)):-:./processCount.pl
2008/06/05 21:47:28 NB000I Source file "maxwell.nb" included. size=273
2008/06/05 21:47:28 NB000I NodeBrain nb[7621,7060]daemonizing
$
Ok, that's better, it daemonized. That just means it disconnected from the terminal and is running in the background with no stdin or stdout. It actually forks a new process to run as the daemon and then ends.
$ ps -ef | grep nb
myuser 7622 1 0 21:47 ? 00:00:00 nb -d maxwell.nb
Well, we have an agent running now, but we don't have anyway to see what it is doing. To give us better visibility we need to instruct NodeBrain to write to a log file. While we're at it, let's make this an executable script by adding a she-bang line. Guess we should stop the running agent also.
$ kill 7622
Here's our enhanced agent we'll call james.nb.
#!/usr/local/bin/nb -d
# james.nb
set log="james.log";
define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
# check process count every minute
define myProcessRule3 on(~(m)):-:./processCount.pl
If we want to execute it we need to set the file permissions.
$chmod 755 james.nb
Now let's start it.
$ ./james.nb
2008/06/05 21:55:31 NB000I Argument [1] -d
2008/06/05 21:55:31 NB000I Argument [2] ./james.nb
> #!/usr/local/bin/nb -d
> # james.nb
> set log="james.log";
2008/06/05 21:55:31 NB000I NodeBrain nb will log to james.log
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
> define myProcessRule2 on(processes<100):-./alarmMe.pl 2 "ok - under 100 processes."
> # check process count every minute
> define myProcessRule3 on(~(m)):-:./processCount.pl
2008/06/05 21:55:31 NB000I Source file "./james.nb" included. size=370
2008/06/05 21:55:31 NB000I NodeBrain nb[7688,7060] daemonizing
$
Now we can see what our agent is doing by tailing the log file. Wait a minute, it isn't doing much. Yes, wait another minute.
$ tail james.log
2009/01/28 09:04:26 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[5619] Started: -:./processCount.pl
[5619: assert processes=75;
2009/01/28 09:04:26 NB000I Rule myProcessRule2 fired
: -./alarmMe.pl 2 "ok - under 100 processes."
[5623] Started: -./alarmMe.pl 2 "ok - under 100 processes."
[5623] Exit(0)
[5619] Exit(0)
2009/01/28 09:05:00 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[5648] Started: -:./processCount.pl
[5648: assert processes=78;
[5648] Exit(0)
2009/01/28 09:06:00 NB000I Rule myProcessRule3 fired
: -:./processCount.pl
[5659] Started: -:./processCount.pl
[5659: assert processes=78;
[5659] Exit(0)
The log tells us the scheduled process is running and there are 78 active processes on this system. If the count goes over 200 we will be notified by rule myProcessRule1 via the alarmMe.pl script.
That's it — we have a working agent. But clearly this example was not designed to convince you that NodeBrain should be used when the application is this simple. This entire problem could be solved with one simple script written in your favorite scripting language. NodeBrain is appropriate for situations where there is a benefit to separating declarative rules from procedural logic in a modular fashion. It may take experimentation with more complex and evolving agent requirements to appreciate when it is appropriate.
I like nonsense; it wakes up the brain cells. – Dr. Seuss (1904 -1991)
What could be more nonsensical than a program called NodeBrain? And of course, if we claim this program is a brain, there must have brain cells and a way to wake them up. So here goes.
You can think of a cell as a structure capable of storing a value and optionally a formula for computing a value. You may be familiar with this notion if you use a spreadsheet program. A NodeBrain cell is vary similar to a spreadsheet cell, but without the tabular model. There is no physical relationship between NodeBrain cells; no notions like "two to the left" or "one cell up". NodeBrain cells are referenced only by their formula, which we call a cell expression.
Cell expressions are allowed in various places within the command syntax. We can't tell you where exactly because we promised not to get BNF on you here, but we can say cell definitions, assertions, and rule conditions are the primary places where cell expressions are used.
The simplest cell expressions identify a literal number or string. Here we define a cell a with the expression 5, and a cell b with the expression "abc".
$ nb
> define a cell 5;
> define b cell "abc";
> show a
a = 5
> show b
b = "abc"
>
We can do simple arithmetic in a cell expression.
$ nb
> define a cell 5+4;
> define b cell a+3;
> show a
a = ! == (5+4)
> show b
b = ! == (a+3)
>
Wait, this must not be what Dr. Seuss meant by nonsense because these brain cells don't seem to be awake! NodeBrain accepted our expression 5+4 but didn't compute 9. This is because NodeBrain is lazy. Nobody cares what the value of a or b is, so NodeBrain doesn't bother to compute them.
We need more nonsense to wake these brain cells up.
Let's try adding a rule that depends on the value of b.
> define rule1 on(b<20);
> show a
a = 9 == (5+4)
> show b
b = 12 == (a+3)
When we added the rule referencing b, NodeBrain was motivated to compute the value of a+3. To get the value of a, it computes the value of 5+4.
The symbol == can be read as "is defined as". The response to show b above can be read as "b is 12 and is defined as a+3."
Just for fun, let's see what happens when we assert new cell expressions for a.
> assert a=19;
> show a
a = 19
> show b
b = 22 == (a+3)
> assert a=1;
2008/06/05 22:04:51 NB000I Rule rule1 fired
> show b
b = 4 == (a+3)
> assert a=5*15/7+2;
> show a
a = 12.71428571
>
Although we are on the topic of arithmetic, it is perhaps less interesting that NodeBrain does arithmetic than when it does it.
When we asserted that a=1, NodeBrain recomputed b and got 4. It then recomputed b<20 which transitioned from false to true and rule1 fired.
Logic is like the sword – those who appeal to it shall perish by it. – Samuel Butler (1835 - 1902)
One can appeal to logic in cell expressions, much as we do in other programming languages. Some familiar operators are illustrated here.
> assert a=1,b=2,c=3,d=3;
> define x cell a>b;
> define rule2 on(a=b or b=c or c>25);
> define rule3 on(a=21 and (b>5 or c<2));
> define rule4 on(x and not a<5.73);
> define rule5 on(a=b and c=d);
Relational operators (> , < , <> , =, >=, <=) have precedence over Boolean operators (and, or, not). Use parentheses to force the order of precedence when you don't want the default or don't know the default.
As we demonstrated with arithmetic expressions, NodeBrain evaluates logical expressions when there are assertions that change the value of operands. For example, consider the following expression used by rule5.
a=b and c=d
If we assert that a=3, the interpreter has to recomputed a=b, but doesn't recompute the larger expression a=b and c=d. Since c and d didn't change, the value of c=d has not changed, and since a=b remains false, the value of the larger expression is known to be unchanged without further computation.
There is only one cell for computing any given expression.
No matter how may rule conditions reference a=b, there is only one a=b cell, as there is only one a cell and one b cell.
Every cell knows which cells reference it.
In this example both a and b know that a=b depends on them.
If a changes, it reports a change to a=b.
If b changes, it does the same.
If a changes, b changes, or both a and b change in the same assertion, a=b is recomputed once.
If a=b changes, expressions that reference it are recomputed.
How do you think the interpreter would respond to the following assertion?
assert a=21,b=21.
Because either of a or b has changed (in this case both), the interpreter must evaluate a=b again.
Since the value is still true, no further computation is required.
I must govern the clock, not be governed by it. – Golda Meir (1898 - 1978)
NodeBrain is happy to be governed by the clock. It has an extensive algebra for expressing schedules we call time conditions. A time condition is a function of time that is true or false at any given second.
In the spirit of this tutorial, we will be deliberately incomplete here and just try to give you a sense of it. The author is somewhat proud of this feature, so conscious restraint is required, but entirely appropriate. (If you need a cure for insomnia, read about time conditions in the NodeBrain Language Reference.)
We'll just jump into it with a few examples. We have a script called schedule.nb with rules whose names describe their time condition. Let's start it in interactive mode.
$ nb schedule.nb -
2008/06/06 06:38:14 NB000I Argument [1] schedule.nb
> define FourOClockHour on(~(h(4)));
> define MondayWednesayAndFriday on(~(mo,we,fr));
2008/06/06 06:38:14 NB000I Rule MondayWednesayAndFriday fired
> define Four2FiveMonWedFri on(~(h(4).(mo,we,fr)));
> define January9 on(~(jan(9)));
> define WeekOfJanuary15 on(~(w.jan(15)));
> define ThursdayOfWeekOfJanuary15 on(~(th.w.jan(15)));
> define LastFridayInJanuary on(~(fr[-1]jan));
> define TuesdayOfWeekOfLastFridayInJanuary on(~(tu.w.fr[-1]jan));
2008/06/06 06:38:14 NB000I Source file "schedule.nb" included. size=371
2008/06/06 06:38:14 NB000I Argument [2] -
2008/06/06 06:38:14 NB000I Reading from standard input.
---------- --------
>
To see the next event for each schedule we use the show command with a /c (clock) option.
We issued this command at 6:41 on Friday June 6, 2008. The h(4) condition will transition to true at 04:00, and then back to false at 05:00. The (mo,we,fr) condition will transition to true at 00:00 Monday the 9th. See if you can make sense out of the rest of the conditions in this list, given the hint that weeks start at 00:00 Sunday. Match the conditions to the name of the corresponding rule above if you need help.
> show /c
~ 2008/06/06 06:41:17 Clock
~ 2008/06/07 00:00:00 ~(mo,we,fr)
on(~(mo,we,fr));
~ 2008/06/07 04:00:00 ~(h(4))
on(~(h(4)));
~ 2008/06/09 04:00:00 ~(h(4).(mo,we,fr))
on(~(h(4).(mo,we,fr)));
~ 2009/01/09 00:00:00 ~(jan(9))
on(~(jan(9)));
~ 2009/01/11 00:00:00 ~(w.jan(15))
on(~(w.jan(15)));
~ 2009/01/15 00:00:00 ~(th.w.jan(15))
on(~(th.w.jan(15)));
~ 2009/01/27 00:00:00 ~(tu.w.fr[-1]jan)
on(~(tu.w.fr[-1]jan));
~ 2009/01/30 00:00:00 ~(fr[-1]jan)
on(~(fr[-1]jan));
A forecast command is used to see when a time condition will transition to true (left column) and false (right column).
> forecast ~(mo,we,fr)
mo,we,fr schedule ~0-0 interval=0,duration=0)
fr 2008/06/06 00:00:00 1212735600 - sa 2008/06/0700:00:00 1212822000
mo 2008/06/09 00:00:00 1212994800 - tu 2008/06/1000:00:00 1213081200
we 2008/06/11 00:00:00 1213167600 - th 2008/06/1200:00:00 1213254000
fr 2008/06/13 00:00:00 1213340400 - sa 2008/06/1400:00:00 1213426800
mo 2008/06/16 00:00:00 1213599600 - tu 2008/06/1700:00:00 1213686000
we 2008/06/18 00:00:00 1213772400 - th 2008/06/1900:00:00 1213858800
> forecast ~(h(4));
h(4) schedule ~0-0 interval=0,duration=0)
sa 2008/06/07 04:00:00 1212836400 - sa 2008/06/0705:00:00 1212840000
su 2008/06/08 04:00:00 1212922800 - su 2008/06/0805:00:00 1212926400
mo 2008/06/09 04:00:00 1213009200 - mo 2008/06/0905:00:00 1213012800
tu 2008/06/10 04:00:00 1213095600 - tu 2008/06/1005:00:00 1213099200
we 2008/06/11 04:00:00 1213182000 - we 2008/06/1105:00:00 1213185600
th 2008/06/12 04:00:00 1213268400 - th 2008/06/1205:00:00 1213272000
The fundamental concepts to grasp here are that time conditions have
mo for Monday, h for hour, jan for January, and w for week that define a set of time intervals, and
, , . , (n), and [n] that operate on time interval sets to produce new sets.
The operators are based on formal concepts like intersection, union, selection, rejection, inverse, etc.
An expert knows all the answers – if you ask the right questions. – Unknown
A NodeBrain node is an expert, with a set of knowledge and associated skill. The skill is provided by a node module (plug-in) that extends NodeBrain functionality. Since NodeBrain is not a general purpose scripting language, there are many things that are simply inappropriate to attempt without help from a node module. It is the combination of skill and knowledge that makes a node an expert. Of course we need to know how to ask the right questions.
Node modules can be developed to perform any function imaginable, but within the context of a cell expression they only provide us with the value of node conditions. These are expressions that look like a function call. In rule r1 below, sally(x) is a node condition and sally is a node with the skills provided by the tree module.
$ nb sally.nb
2008/06/06 07:05:49 NB000I Argument [1] sally.nb
> # sally.nb
> define sally node tree;
> assert sally("abc"),sally("def",5)=2;
> show sally
sally = ! == node tree
"def"
5=2
"abc"=1
> define r1 on(sally(x));
> assert x="abc";
2008/06/06 07:05:49 NB000I Rule r1 fired
Do you see why rule r1 fired when we asserted that x="abc"? It is because sally("abc") was asserted to be true, so sally(x) is true when x="abc". What a node module does with assertions and how it computes the value of node conditions is entirely up to it. In this case, the tree node module keeps track of assertions and returns a value for conditions by doing a lookup. When the value of x changes, the interpreter reports a change to the node module and it computes a new value for the cell.
When the cell value changes, it reports a change to the referencing cells, in this case rule r1, which fired.
A complex system that works is invariably found to have evolved from a simple system that works. – John Gaule
Every type of cell expression introduced in this tutorial can be combined into complex expressions as illustrated by rule complex below.
$ nb complex.nb
2008/06/06 07:14:28 NB000I Argument [1] complex.nb
> # complex.nb
> define sally node tree;
> assert sally("abc"),sally("def",5)=2;
> define complex on(sally(x,y)=a+b and y<7 and ~(fr));
> assert y=5;
> assert a,b;
> assert x="def";
2008/06/06 07:14:28 NB000I Rule complex fired
If the complex rule doesn't fire when you run complex.nb, perhaps you are not running it on a Friday.
If you try it on a Friday, it will fire because y<7 is true and sally(x,y)=a+b is true.
The fact that the last expression is true may not be so obvious, so let's break it down. The command assert a,b;
sets a and b to 1 (true). That means the expression a+b is 2. Since x is "def" and y is 5, the expression sally(x,y)=a+b is sally("def",5)=2, which we assert to be true.
Ignorance more frequently begets confidence than does knowledge. – Charles Darwin
This tutorial is about ignorance; when NodeBrain is aware of it and how it is handled. It is perhaps obvious, but important to remember, that NodeBrain assumes that whatever is asserted is the truth. It makes no attempt to independently verify assertions, or check to see if assertions should have been made that were not. So what NodeBrain thinks it knows may not be the truth. In this respect, NodeBrain is unaware of its ignorance and blissfully acts with great confidence. No different than most of us.
However, within NodeBrain's own version of reality, it can be aware of a form of ignorance. The notion here is that something can be known to be unknown, a possibility often dismissed by people rushing to judgment. How NodeBrain deals with that condition is the topic of this tutorial.
Something unknown is doing we don't know what. – Sir Arthur Eddington (1882 - 1944)
The NodeBrain interpreter recognizes three logical states: true, false, and unknown. These states are represented as follows.
Value Representation Unknown ? False 0 True 1 or anything else (e.g. 5.2,-7, "abc", "")
We can test for these states by using the = relational operator or a prefix operator. The ! prefix operator means "not" and the ? prefix operator means "is unknown".
> define r1 on(a=1 and b=0 and c=? and x and !y and ?z);
Values can be asserted similarly.
$ nb
> assert a=1,b=0,c=?,x,!y,?z;
> show -t
@ =! == node
z =?
y =0
x =1
c =?
b =0
a =1
>
Although anything other than ? or 0 is interpreted as true, logical operators often return a value of 1 to represent a true state. Below we see that the value of a<b is 1.
> assert a=5,b=7;
> define r1 on(a<b);
> show (a<b)
()= 1 == (a<b)
Here's the important thing to understand. Logical operators accept the unknown value as an operand and return the unknown value under specific conditions. Logic tables for A and B and A or B are shown below. Notice that in some cases we know the result without knowing both operands.
AND A B A & B 0 0 0 0 1 0 0 ? 0 1 0 0 1 1 1 1 ? ? ? 0 0 ? 1 ? ? ? ?
OR A B A | B 0 0 0 0 1 1 0 ? ? 1 0 1 1 1 1 1 ? 1 ? 0 ? ? 1 1 ? ? ?
The ? prefix operator enables us to test for an unknown condition and act upon it.
$ nb
> define x cell a+b;
> define r1 on(?x);
> assert a=7,b=12;
> show -t
@ =! == node
r1= ! == on(?x);
x =19 == (a+b)
b =12
a =7
> assert ?b;
2008/06/0607:41:34 NB000I Rule r1 fired
>
Let's pause to make sure we all know what just happened here. The condition ?x is true when x is unknown. This was true when we first defined r1 because x is a+b and both a and b are unknown at that time. Hey, the arithmetic operators understand the unknown value also. When we asserted that a=7 and b=12, x was known to be 19 and ?x was false. Then we assert ?b, making a+b unknown, making x unknown, making ?x true and r1 fired.
Some systems use a closed world assumption, where anything not known to be true is assumed to be false. As we illustrated in the previous section, NodeBrain does not generally make this assumption. Instead it allows the unknown state to propagate through the evaluation of cell expression.
However, node modules are allowed to handle the unknown state using either the closed world assumption or three-state logic. This means you must be aware of how each node module handles the unknown state when coding rules.
We'll illustrate using two nodes, one with the tree skill and one with the cache skill. The tree module uses three-state logic and the cache module uses the closed world assumption.
$ nb closedWorld.nb
2008/06/0607:44:05 NB000I Argument [1] closedWorld.nb
> define myTree expert tree;
> define myCache expert cache:(soup);
> define rMyTreeHasSoup on(myTree(soup));
> define rMyCacheHasSoup on(myCache(soup));
> define rMyTreeHasNotSoup on(!myTree(soup));
> define rMyCacheHasNotSoup on(!myCache(soup));
> define rMyTreeKnowsNotSoup on(?myTree(soup));
> define rMyCacheKnowsNotSoup on(?myCache(soup));
> assert myTree("Chicken"),myCache("Chicken");
> assert soup="Tomato";
> assert soup="Chicken";
2008/06/0607:44:05 NB000I Rule rMyCacheHasSoup fired
2008/06/0607:44:05 NB000I Rule rMyTreeHasSoup fired
> assert ?myTree("Chicken"),?myCache("Chicken");
2008/06/0607:44:05 NB000I Rule rMyTreeKnowsNotSoup fired
2008/06/0607:44:05 NB000I Rule rMyCacheHasNotSoup fired
> assert !myTree("Chicken"),!myCache("Chicken");
2008/06/0607:44:05 NB000I Rule rMyTreeHasNotSoup fired
2008/06/0607:44:05 NB000I Source file "closedWorld.nb" included. size=514
2008/06/0607:44:05 NB000I NodeBrain nb[13980] terminating - exit code=0
$
This may just be a blur, so let's take the assertions one at a time. Initially soup is unknown, myTree(soup) is unknown, and myCache(soup) is false.
The reason myCache(soup) is false is because the cache skill uses the closed world assumption; what is not known to be true is false.
When we assert "Chicken" to both the tree and the cache there is no change to the value of myTree(soup) and myCache(soup).
When we assert that soup="Tomato" there is still no change. A value of "Tomato" is not known to the tree or cache, in the same way an unknown soup is not known.
Next we assert that soup="Chicken". Both myTree(soup) and myCache(soup) transition to true and the corresponding rules rMyCacheHasSoup and rMyTreeHasSoup fire.
We then assert myTree("Chicken") and myCache("Chicken") to be unknown. Because the cache skill uses the closed world assumption, this is the same as asserting that myCache("Chicken") is false. Consequently, rMyCacheHasNotSoup fires while myTreeKnowsNotSoup fires.
Finally we assert myTree("Chicken") and myCache("Chicken") to be false. This causes the rule rMyTreeHasNotSoup to fire.
There is no change to myCache("Chicken"), so no cache related rule fires.
The difference between the tree and cache skills is that we must assert myTree(soup) to be false before myTree(soup) evaluates to false, while myCache(soup) is false simply because we haven't asserted it to be true.
Asserting either !myCache("Chicken") or ?myCache("Chicken") removes knowledge of "Chicken" from the cache.
Only an assertion of ?myTree("Chicken") removes knowledge of "Chicken" from the tree.
Asserting !myTree("Chicken") asserts a value of false to the "Chicken" entry in the tree.
In all the previous examples in this tutorial the interpreter has been operating in reactive mode when processing rules. In this mode, the interpreter waits for assertions and responds appropriately. It makes no effort to convert unknown values into known values. Consider the following example.
$ nb
> define myProcessRule1 on(processes>200):-./alarmMe.pl 1 "dang - more than 200 processes."
>
The interpreter knows the value of processes is unknown and it does nothing to find out. This is reasonable because we haven't asked it to and we haven't told it how. These are possibilities we cover in the next section.
There is an interpreter mode we call query mode, in which NodeBrain attempts to solve for unknown values as required to obtain a known value for rule conditions. This mode approximates what your doctor does when you go in with a medical problem, or your car mechanic does when you take your car in with a mechanical problem. They ask questions of you, and they ask questions of themselves which they try to answer by running tests. They attempt to diagnose your problem by applying rules and seeking values for parameters that are initially unknown. They only ask the questions and run the tests that are necessary to arrive at a conclusion. This helps to avoid running unnecessary expensive tests.
If we tell NodeBrain how to query for unknown values and ask it to solve for rule conditions that have an unknown value, it can diagnose or investigate with similar efficiency.
A query rule file is simply a rule file designed to be processed in query mode. Here's a file named illness.nb that is a trivial (and not useful) query rule file.
# File: tutorial/Ignorance/illness.nb
define CommonCold on(cough and soreThroat);
define Flu on(fever and achy and upsetStomach);
As you can see, these query rules have the same syntax as reactive rules. The difference is not in the rules, but in how we use them.
Let's invoke this script in interactive mode and issue a query command.
$ nb illness.nb -
> # File: tutorial/Ignorance/illness.nb
> define CommonCold on(cough and soreThroat);
> define Flu on(fever and achy and upsetStomach);
2008/06/10 15:51:44 NB000I Source file "illness.nb" included. size=111
2008/06/10 15:51:44 NB000I Argument [2] -
2008/06/10 15:51:44 NB000I Reading from standard input.
---------- --------
> query
(fever&(achy&upsetStomach))
Enter cell fever==0
(cough&soreThroat)
Enter cell cough==1
(cough&soreThroat)
Enter cell soreThroat==1
2008/06/10 15:52:11 NB000I Rule CommonCold fired
>
Since we are running in interactive mode and have not provided any alternate methods for NodeBrain to solve for unknown values, the interpreter prompts the user to resolve unknown values. If you study the rule file you will find 5 unknown terms in the two rule conditions: cough, soreThroat, fever, achy, and upsetStomach. However, the interpreter only prompted the user for values of three of these terms. This is because requesting values for the other two terms would not have changed the result. Since fever=0, the flu condition is false for any value of the other two terms in the condition.
Our response to each query may be any valid cell expression. This means we must enclose strings in quotes, and have the option of introducing new terms for which the interpreter does not have a value. When new terms are introduced, the interpreter attempts to resolve them as necessary. If we respond with 5 or soreEye, the interpreter will not attempt to solve for soreEye because it knows the expression is true without knowledge of soreEye.
In most applications of NodeBrain we are attempting to automate decisions, so we want to query software to resolve unknowns where possible. Here's a script called illnessConsult.pl that simulates the running of potentially complex tests to arrive at the same answers we provided interactively above. There would be no point in doing something this simple for real, and no point in doing something more complicated in a tutorial. You can imagine a much more complicated problem.
#!/usr/bin/perl
# File: tutorial/Ignorance/illnessConsult.pl
$state{"cough"}=1;
$state{"soreThroat"}=1;
$state{"fever"}=0;
$state{"achy"}=1;
$state{"upsetStomach"}=0;
$symptom=shift;
if(exists($state{$symptom})){print("$state{$symptom}\n");}
else{print("?\n");}
Let's run this script that we are pretending to be complicated software to determine the value of the cough symptom.
$./illnessConsult.pl cough
1
Now we'll create a new rule file called illnessConsult.nb to consult our Perl script.
# File: tutorial/Ignorance/illnessConsult.nb
use: illnessConsult.pl
defineCommonCold on(cough and soreThroat);
defineFlu on(fever and achy and upsetStomach);
query;
Now we can diagnose an illness in batch mode.
$ nb illnessConsult.nb
2008/06/1016:12:25 NB000I Argument [1] illnessConsult.nb
> # File: tutorial/Ignorance/illnessConsult.nb
> use : illnessConsult.pl
> define CommonCold on(cough and soreThroat);
> define Flu on(fever and achy and upsetStomach);
> query;
2008/06/1016:12:25 NB000T Resolving "fever" via command : illnessConsult.pl"fever"
Value=(0)
2008/06/1016:12:25 NB000T Resolving "cough" via command : illnessConsult.pl"cough"
Value=(1)
2008/06/1016:12:25 NB000T Resolving "soreThroat" via command :illnessConsult.pl "soreThroat"
Value=(1)
2008/06/1016:12:25 NB000I Rule CommonCold fired
2008/06/1016:12:25 NB000I Source file "illnessConsult.nb" included. size=149
2008/06/1016:12:25 NB000I NodeBrain nb[13690] terminating - exit code=0
$
In this example the consulted script illnessConsult.pl returned simple 0 and 1 values. The interpreter will accept any valid cell expression and will attempt to resolve any term whose value is both unknown and needed.
If you obey all the rules, you miss all the fun. — Katherine Hepburn
In this tutorial we'll see how much fun we can have making up rules and watching NodeBrain obey them.
NodeBrain on, when and if rules have almost identical syntax. Their conditional cell expressions are resolved by the same cell evaluation algorithm. Yet they respond differently to new information. You can study these differences using rules that have no action. Start by creating the following script called rules.nb.
# File: tutorial/RuleTypes/rules.nb
definer0 when(a=1 and b=2);
definer1 on(a=1 and b=2);
definer2 on(a=2 and b=2);
definer3 if(a=1 and b=2);
assert a=1,b=2;
assert a=1,b=2;
assert a=2;
assert a=27;
alert a=1;
alert a=1;
When you execute this script you will notice that rules "fire" in response to assert and alert commands.
$ nb rules.nb
2008/06/1016:54:24 NB000I Argument [1] rules.nb
> # File: tutorial/RuleTypes/rules.nb
> define r0 when(a=1 and b=2);
> define r1 on(a=1 and b=2);
> define r2 on(a=2 and b=2);
> define r3 if(a=1 and b=2);
> assert a=1,b=2;
2008/06/1016:54:24 NB000I Rule r0 fired
2008/06/1016:54:24 NB000I Rule r1 fired
> assert a=1,b=2;
> assert a=2;
2008/06/1016:54:24 NB000I Rule r2 fired
> assert a=27;
> alert a=1;
2008/06/1016:54:24 NB000I Rule r1 fired
2008/06/1016:54:24 NB000I Rule r3 fired
> alert a=1;
2008/06/1016:54:24 NB000I Rule r3 fired
2008/06/1016:54:24 NB000I Source file "rules.nb" included. size=206
2008/06/1016:54:24 NB000I NodeBrain nb[13859] terminating - exit code=0
$
Although rules r0, r1 and r3 are based on the same condition (a=1 and b=2), they react differently to the assert and alert commands.
Q: Why didn't r3 fire on the first "assert a=1,b=2;" command?
A: Because if rules only respond to alert commands.
Q: Why didn't r1 fire on the second "assert a=1,b=2;" command?
A: Because on rules only fire when their condition transitions to true. The second assertion made no change in the state of the condition.
Q: Why didn't r0 fire when r1 fired the second time?
A: Because when rules only fire once. They are removed from the rule set once they fire.
Q: Why did r3 fire on the second "alert a=1;" command?
A: Because if rules fire on every alert command that leaves their condition in a true state. Unlike on and when rules, they are not required to transition to a true state from a false or unknown state.
In the previous example you may have noticed that NodeBrain supports both state and event monitoring. The difference between the two is subtle.
The assert command and on rule are designed for state monitoring.
The alert command and if rule are designed for event monitoring. But it isn't that simple.
The on rule responds to both assert and alert commands. To an on rule, alert is just an alias for assert. In fact, everything about the way an alert command is processed is identical to the way an assert command is processed, except alert brings if rules into play. In other words, alert makes an assertion, and then says, "Oh, and by the way, this is an event to be considered by the if rules."
What makes our notion of state and event monitoring hard to untangle is the fact that we describe an event in the same way we describe a state, interpret an event as a new state, recognize state changes as events, and translate state changes into events. Let's look at these confusing statements individually.
# describe an event in the same way we describe a state
alert a=1,b=2; # event
assert a=1,b=2; # state
# interpret an event as a new state
define r1 on(a=1 and b=2);
define r2 if(a=1 and b=2);
alert a=1,b=2; # recognized as new state to both IF and ON rules
alert a=1,b=2; # recognized as new state to IF rule, not to ON rule
# recognize state changes as events
define r1 on(a=1 and b=2);
assert a=0,b=0;
assert a=1,b=2; # triggers internal events that cause r1 to fire
# translate state changes into events
define r1 on(a=1 and b=2): alert x="abc",y="def";
assert a=1,b=2; # r1 translated state change into event(alert)
This section provides answers to some anticipated questions.
From now on, ending a sentence with a preposition is something up with which I will not put. — Sir Winston Churchill (1874 - 1965)
NodeBrain does not require a semi-colon to terminate a command at the end of a line. The following examples are both acceptable.
> assert a=1,b=2;
> assert a=1,b=2
A semi-colon is required to terminate a command before a comment.
> assert a=1,b=2; # This is valid
> assert a=1,b=2 # This is not valid
You can not put two commands on one line.
> assert a=1,b=2; define x cell 10; # This is not valid
There are cases where a NodeBrain command ends with a command directed to another interpreter. In those cases, you must follow the syntax rules of the other interpreter. This is illustrated by the following rule that uses a host shell command as the action.
> define psNb on(~(m)):-ps -ef | grep nb
There is no support for multiple line commands. A new-line character ("\n") is always interpreted as the end of a command.
If you are specifying multiple shell commands, the shell supports semi-colon ; separated commands on a single line.
> define r1 on(a=1 and b=2):= myscript1.pl; rm*.foo; myscript2.pl
You may also include multiple comma separated NodeBrain assertions.
> define r1 on(a=1 and b=2) x=5,y="abc",z=20 := myscript1.pl;rm *.foo; myscript2.pl
If you need to specify multiple NodeBrain commands, they cannot be specified directly in the action of a rule. But let's look at some alternatives.
If order is important, and you want to specify several commands, place the commands in a separate source file, and use the source command as the rule action.
> define r1 on(a=1 and b=2):source r1Action.nb
You may also use a -: or =: (servant) command and have a servant script present multiple commands to the interpreter.
> define r1 on(a=1 and b=2):-:r1Action.pl
If the order of the commands is not important, and the number is relatively small, you can use multiple rules with the same condition. To avoid typing the condition multiple times, specify the name of the first rule as the second rule's condition.
> define r1 on(a=1 and b=2):-myscript1.pl
> define r2 on(r1): sally. alert x=5;
> define r3 on(r2): source r3Action.nb
Under the current implementation, you should not expect these rules to fire in any particular order.
This is not allowed. Because NodeBrain is not a general purpose or report writing language, we have not yet found a reason to support quotes within a string. When generating cell expressions manually or with a program, simply avoid generating values containing quotes.
No, and we like it that way. We have not found it necessary to use special characters in NodeBrain strings. In commands sent to the host shell, and messages sent to servants or node modules, NodeBrain does not interfere with the use of any escape sequences supported by the target interpreter. But NodeBrain is oblivious to it, and this is actually why there is no interference. If a string value assigned to a NodeBrain term is ultimately destined to another interpreter (shell or node module), then you are free to include an escape sequence (except an escaped double quote). Again, NodeBrain is oblivious to it.
A memory is what is left when something happens and does not completely unhappen. — Edward de Bono
A tree node provides NodeBrain with a place to remember things that happen until they unhappen. Actually that is true of simple cells as well. The difference is that simple cells store one value while trees store multiple associated values. So a tree can remember a more complex happening. You can think of a tree node as a table of values. Like a relational table, the values in a given row form a relation. But we represent the table using a tree-of-trees structure and associate a value with each relation, including each left-to-right partial relation. This enable us to test for the existence of a relation, or use a relation as a key for looking up a stored value.
An expression of the form tree(cell1,cell2,...) can be used like a term in cell expressions.
We can assert values for these expressions and use then as conditions.
The stuff.nb file below illustrates these concepts.
#!/usr/local/bin/nb
# File: tutorial/tree/stuff.nb
define stuff node tree;
define r1 on(stuff(x,y)>17 and stuff(x,y,z)="Sally");
stuff. assert ("Kathy","apple")=2,(1,2,3)="Sally",(1,2)=27;
assert x=1,y=2;
assert z=3;
show stuff
assert ?stuff(1,2);
show stuff
assert ?stuff(1,2,3);
show stuff
assert a=stuff("Kathy","apple");
show a
Study the execution below.
Do you know why rule r1 fires when it does?
Try rewriting the rule by replacing x, y, and z with their values at the time the rule fires.
Then replace the stuff(...) expressions in the rule with their values at the time the rule fires.
$ ./stuff.nb
2009/01/31 12:36:15 NB000I Argument [1] ./stuff.nb
> #!/usr/local/bin/nb
> # File: tutorial/tree/stuff.nb
> define stuff node tree;
> define r1 on(stuff(x,y)>17 and stuff(x,y,z)="Sally");
> stuff. assert ("Kathy","apple")=2,(1,2,3)="Sally",(1,2)=27;
> assert x=1,y=2;
> assert z=3;
2009/01/31 12:36:15 NB000I Rule r1 fired
> show stuff
stuff = ? == node tree
"Kathy"
"apple"=2
1
2=27
3="Sally"
> assert ?stuff(1,2);
> show stuff
stuff = ? == node tree
"Kathy"
"apple"=2
1
2
3="Sally"
> assert ?stuff(1,2,3);
> show stuff
stuff = ? == node tree
"Kathy"
"apple"=2
> assert a=stuff("Kathy","apple");
> show a
a = 2
2009/01/31 12:36:15 NB000I Source file "./stuff.nb" included. size=301
2009/01/31 12:36:15 NB000I NodeBrain nb[24913] terminating - exit code=0
$
Sometimes we use a tree node to specify a set of values to watch. Here we use a simple tree to represent a list, or a table with a single column. A multi-column table may be used if you want to watch for specific combinations of event attributes.
#!/usr/local/bin/nb
# File: tutorial/tree/watch.nb
define watchtype node tree;
watchtype. assert ("tree fell"),("log rolled"),("frog croaked");
define r1 if(watchtype(type));
# Sample events
alert type="tide out";
alert type="log rolled";
alert type="tide in";
alert type="frog croaked";
Notice how the following execution picks out the events of interest.
./watch.nb
2009/01/31 12:56:27 NB000I Argument [1] ./watch.nb
> #!/usr/local/bin/nb
> # File: tutorial/tree/watch.nb
> define watchtype node tree;
> watchtype. assert ("tree fell"),("log rolled"),("frog croaked");
> define r1 if(watchtype(type));
> # Sample events
> alert type="tide out";
> alert type="log rolled";
2009/01/31 12:56:27 NB000I Rule r1 fired
> alert type="tide in";
> alert type="frog croaked";
2009/01/31 12:56:27 NB000I Rule r1 fired
2009/01/31 12:56:27 NB000I Source file "./watch.nb" included. size=289
2009/01/31 12:56:27 NB000I NodeBrain nb[26148] terminating - exit code=0
$
We said earlier that a tree can be used to remember an event between the time it happens and unhappens. Let's look at an example that provides a better illustration of this.
#!/usr/local/bin/nb
# File: tutorial/tree/sequence.nb
define jumped node tree;
define r1 if(event="jump" and ?jumped(name)) jumped(name);
define r2 if(event="land" and jumped(name)) ?jumped(name);
define r3 if(event="jump" and jumped(name)):$ # ${name} jumped twice without intervening landing
define r4 if(event="land" and ?jumped(name)):$ # ${name} landed twice without intervening jump
# Sample events
alert event="jump",name="sally";
alert event="jump",name="joe";
alert event="land",name="sally";
alert event="land",name="joe";
alert event="jump",name="sally";
alert event="land",name="joe";
alert event="jump",name="sally";
In the execution below we were able to detect a couple things that happened and then happened again without first unhappening.
2009/01/31 13:22:07 NB000I Argument [1] ./sequence.nb
> #!/usr/local/bin/nb
> # File: tutorial/tree/sequence.nb
> define jumped node tree;
> define r1 if(event="jump" and ?jumped(name)) jumped(name);
> define r2 if(event="land" and jumped(name)) ?jumped(name);
> define r3 if(event="jump" and jumped(name)):$ # ${namer} jumped twice without an intervening landing
> define r4 if(event="land" and ?jumped(name)):$ # ${name} landed twice without an intervening jump
> # Sample events
> alert event="jump",name="sally";
2009/01/31 13:22:07 NB000I Rule r1 fired (jumped(name)=1)
> alert event="jump",name="joe";
2009/01/31 13:22:07 NB000I Rule r1 fired (jumped(name)=1)
> alert event="land",name="sally";
2009/01/31 13:22:07 NB000I Rule r2 fired (jumped(name)=?)
> alert event="land",name="joe";
2009/01/31 13:22:07 NB000I Rule r2 fired (jumped(name)=?)
> alert event="jump",name="sally";
2009/01/31 13:22:07 NB000I Rule r1 fired (jumped(name)=1)
> alert event="land",name="joe";
2009/01/31 13:22:07 NB000I Rule r4 fired
: # joe landed twice without an intervening jump
> alert event="jump",name="sally";
2009/01/31 13:22:07 NB000I Rule r3 fired
: # sally jumped twice without an intervening landing
2009/01/31 13:22:07 NB000I Source file "./sequence.nb" included. size=631
2009/01/31 13:22:07 NB000I NodeBrain nb[26552] terminating - exit code=0
$
A tree node can support large trees if you have enough memory on your system. From a performance perspective tree nodes scale well because they are organized as binary trees that are kept reasonably balanced. A lookup on a tuple (x,y,z) is performed as a binary search for x, followed by a binary search for y in a tree owned by (x), followed by a binary search for z in a tree owned by (x,y).
There is a measure in everything. There are fixed limits beyond which and short of which right cannot find a resting place. — Horace (65 BC - 8 BC)
A cache node, like a tree node, provides a place to remember events as associations, or relations. But a cache adds to this the ability to measure repetition and variation within intervals of time and respond when predefined limits are reached. It enables us to detect patterns of events that can be described as follows.
1) X happened N times within time period P or interval I.2) X was associated with Y in N events within time period P or interval I.
3) X was associated with N different values of Y within time period P or interval I.
4) X happened within interval I after Y happened.
Here X and Y represent the values of a set of one or more event attributes (A, B, C, ...). For example, X might represent a (type, city) tuple while Y represents a (customer, item, quantity) tuple. If you think of a cache as a table, which is sometimes a good way to visualize it, a tuple is just a table row. If you think of a cache as a table-of-tables, a better way to visualize it, then Y is a row in a table associated with row X.
Here's a 24-hour cache node with 4 thresholds and 4 rules to respond when they are reached.
#!/usr/local/bin/nb
# File: tutorial/cache/cache.nb
define horace node cache:(~(24h):type(20),city{10}[5],customer,item,quantity(3));
horace. define r1 if(type._hitState): $ # There have been ${type._hits} ${type} events
horace. define r2 if(city._rowState): $ # (${type},${city}) had ${city._rows} different events
horace. define r3 if(city._kidState): $ # (${type},${city}) had ${city._kids} different customers
horace. define r4 if(quantity._hitState): $ # (${type},${city},${customer},${item},${quantity}) happened ${quantity._hits} times
Here's a set of events represented as assertions to our 24-hour cache node named "horace".
# File: tutorial/cache/events.nb
horace. assert ("purchase","Paris","Bruno","iPod",5);
horace. assert ("purchase","Paris","Bruno","iPod",5);
horace. assert ("purchase","Paris","Bruno","iPod",5);
horace. assert ("purchase","Paris","Bruno","shirt",2);
horace. assert ("purchase","Paris","Bruno","shoes",1);
horace. assert ("purchase","Paris","Madeleine","shoes",5);
horace. assert ("purchase","Paris","Madeleine","skirt",1);
horace. assert ("purchase","Paris","Madeleine","bread",2);
horace. assert ("purchase","Paris","Jeannine","bread",1);
horace. assert ("purchase","Paris","Laure","bread",1);
horace. assert ("purchase","Paris","Henri","iPod",1);
horace. assert ("return","Paris","Henri","iPod",1);
horace. assert ("return","Paris","Madeleine","iPod",1);
horace. assert ("purchase","London","Abigail","milk",1);
horace. assert ("purchase","London","Addie","bread",1);
horace. assert ("purchase","London","Alston","bread",1);
horace. assert ("purchase","London","Alston","candy",1);
horace. assert ("purchase","London","Alston","candy",2);
horace. assert ("purchase","London","Alston","candy",3);
horace. assert ("purchase","London","Alston","candy",4);
horace. assert ("purchase","London","Alston","candy",5);
horace. assert ("purchase","London","Alston","candy",6);
horace. assert ("purchase","London","Alston","candy",7);
horace. assert ("purchase","London","Alston","candy",8);
horace. assert ("purchase","London","Alston","candy",9);
Here's a sample execution.
$ ./cache.nb events.nb
2009/01/31 09:10:04 NB000I Argument [1] ./cache.nb
> #!/usr/local/bin/nb
> # File: tutorial/cache/cache.nb
> define horace node cache:(~(24h):type(20),city{10}[5],customer,item,quantity(3));
> horace. define r1 if(type._hitState): $ # There have been ${type._hits} ${type} events
> horace. define r2 if(city._rowState): $ # (${type},${city}) had ${city._rows} different events
> horace. define r3 if(city._kidState): $ # (${type},${city}) had ${city._kids} different customers
> horace. define r4 if(quantity._hitState): $ # (${type},${city},${customer},${item},${quantity}) happened ${quantity._hits} times
2009/01/31 09:10:04 NB000I Source file "./cache.nb" included. size=511
2009/01/31 09:10:04 NB000I Argument [2] events.nb
> # File: tutorial/cache/events.nb
> horace. assert ("purchase","Paris","Bruno","iPod",5);
> horace. assert ("purchase","Paris","Bruno","iPod",5);
> horace. assert ("purchase","Paris","Bruno","iPod",5);
2009/01/31 09:10:04 NB000I Rule horace.r4 fired
: horace. # (purchase,Paris,Bruno,iPod,5) happened 3 times
> horace. assert ("purchase","Paris","Bruno","shirt",2);
> horace. assert ("purchase","Paris","Bruno","shoes",1);
> horace. assert ("purchase","Paris","Madeleine","shoes",5);
> horace. assert ("purchase","Paris","Madeleine","skirt",1);
> horace. assert ("purchase","Paris","Madeleine","bread",2);
> horace. assert ("purchase","Paris","Jeannine","bread",1);
> horace. assert ("purchase","Paris","Laure","bread",1);
> horace. assert ("purchase","Paris","Henri","iPod",1);
2009/01/31 09:10:04 NB000I Rule horace.r3 fired
: horace. # (purchase,Paris) had 5 different customers
> horace. assert ("return","Paris","Henri","iPod",1);
> horace. assert ("return","Paris","Madeleine","iPod",1);
> horace. assert ("purchase","London","Abigail","milk",1);
> horace. assert ("purchase","London","Addie","bread",1);
> horace. assert ("purchase","London","Alston","bread",1);
> horace. assert ("purchase","London","Alston","candy",1);
> horace. assert ("purchase","London","Alston","candy",2);
> horace. assert ("purchase","London","Alston","candy",3);
> horace. assert ("purchase","London","Alston","candy",4);
> horace. assert ("purchase","London","Alston","candy",5);
> horace. assert ("purchase","London","Alston","candy",6);
2009/01/31 09:10:04 NB000I Rule horace.r1 fired
: horace. # There have been 20 purchase events
> horace. assert ("purchase","London","Alston","candy",7);
2009/01/31 09:10:04 NB000I Rule horace.r2 fired
: horace. # (purchase,London) had 10 different events
> horace. assert ("purchase","London","Alston","candy",8);
> horace. assert ("purchase","London","Alston","candy",9);
2009/01/31 09:10:04 NB000I Source file "events.nb" included. size=1407
2009/01/31 09:10:04 NB000I NodeBrain nb[27523] terminating - exit code=0
$
Ok, now why did rules r1, r2, r3, and r4 fire when they did? Each time we assert a tuple to the cache, it updates a tree-of-trees structure as needed to retain the full set of asserted tuples. It also updates three counters at each node within the tree-of-trees: hits, rows, and kids. We define hits as the number of times an assertion arrives at a given node within the tree, rows as the number of subordinate table rows represented by the subordinate tree-of-trees, and kids as the number of directly subordinate nodes—or the number of unique values in the first column of the subordinate table. In our definition of the "horace" cache node, we specified thresholds for hits using (), rows using {}, and kids using []. When a threshold is reached, the cache node alerts itself. The rules we defined for the node handle the alerts. In the example above, when r4 fired, it was responding to an invisible alert that would look something like the following if it were not invisible.
horace. alert quantity._hitState,quantity._hits=3,type="purchase",city="Paris",customer="Bruno",item="iPod",quantity=5;
This alert was triggered because the hit counter for the (purchase,Paris,Bruno,iPod,5) node in our cache reached the specified limit of 3 within a 24-hour interval. See if you can figure out why rules r1, r2, and r3 fired when they did.
In this example our rules just issue comment commands. In a real application the rules would have taken action of some kind: an alert, assertion, alarm, shell command, etc.
A tuple in a cache can be retained for a defined time interval (e.g. 24-hours in our example above), until the end of a time period (e.g. end of current minute, hour, day), until some state is detected, or indefinitely.
define horace node cache:(~(24h):type(20),city{10}[5],customer,item,quantity(3)); [interval]
define horace node cache(~(d)):(type(20),city{10}[5],customer,item,quantity(3)); [end of period]
define horace node cache(a=1):(type(20),city{10}[5],customer,item,quantity(3)); [state a=1]
define horace node cache:(type(20),city{10}[5],customer,item,quantity(3)); [indefinitely]
We can also remove a tuple or set of tuples from a cache at any time. The following command removes all tuples starting with ("purchase","London").
horace. assert ?("purchase","London");
In addition to alerting when a threshold is reached, we can direct a cache to alert when a tuple expires. This enables us to assert a tuple to the cache and take action if we haven't asserted it again within the expiration period. In other word, we can use a cache to know when something hasn't happened for some interval of time.
#!/usr/local/bin/nb
# File: tutorial/cache/tardy.nb
define tardy node cache:(!~(6s):Source);
tardy. define r1 if(_action="expire"): $ # ${Source} has been quiet for ${_interval}
tardy. assert ("Fred");
When executed below, we are notified that "Fred" has not been asserted to our cache for 6 seconds.
./tardy.nb -
2009/01/31 10:38:06 NB000I Argument [1] ./tardy.nb
> #!/usr/local/bin/nb
> # File: tutorial/cache/tardy.nb
> define tardy node cache:(!~(6s):Source);
> tardy. define r1 if(_action="expire"): $ # ${Source} has been quiet for ${_interval}
> tardy. assert ("Fred");
> # Press the ENTER key once repeatedly until the rule fires
> # Should happen in 6 seconds
2009/01/31 10:38:06 NB000I Source file "./tardy.nb" included. size=290
2009/01/31 10:38:06 NB000I Argument [2] -
2009/01/31 10:38:06 NB000I Reading from standard input.
---------- --------
>
>
>
>
>
>
2009/01/31 10:38:12 NB000I Rule tardy.r1 fired
: tardy. # Fred has been quiet for 6 seconds
>
A cache node can be used like a tree node for detecting a sequence of events. However, the cache node, having support for scheduled tuple expiration, can also support a timing condition.
The OnJust cache below is used to remember for 5 seconds that a switch has been turned on.
The TurnedOn rule asserts the name of a switch to the cache each time a switch is turned on.
The TurnedOff rule responds to a switch being turned off if the cache still remembers the switch being turned on.
#!/usr/local/bin/nb
# File: tutorial/cache/sequence.nb
define OnJust node cache:(~(5s):switch);
define TurnedOn if(on) OnJust(switch);
define TurnedOff if(!on and OnJust(switch)): $ # The ${switch} turned off within ${OnJust._interval} of turning on
# Sample events
alert on,switch="kitchen light";
alert on,switch="porch light";
alert !on,switch="kitchen light";
-sleep 6
alert !on,switch="porch light";
alert on,switch="porch light";
alert !on,switch="porch light";
In the execution below, you will notice that the TurnedOff rule did not respond when the porch light stayed on for 6 seconds.
$ ./sequence.nb
2009/01/31 11:23:14 NB000I Argument [1] ./sequence.nb
> #!/usr/local/bin/nb
> # File: tutorial/cache/sequence.nb
> define OnJust node cache:(~(5s):switch);
> define TurnedOn if(on) OnJust(switch);
> define TurnedOff if(!on and OnJust(switch)): $ # The ${switch} turned off within ${OnJust._interval} of turning on
> alert on,switch="kitchen light";
2009/01/31 11:23:14 NB000I Rule TurnedOn fired (OnJust(switch)=1)
> alert on,switch="porch light";
2009/01/31 11:23:14 NB000I Rule TurnedOn fired (OnJust(switch)=1)
> alert !on,switch="kitchen light";
2009/01/31 11:23:14 NB000I Rule TurnedOff fired
: # The kitchen light turned off within 5 seconds of turning on
> -sleep 6
[6410] Started: -sleep 6
[6410] Exit(0)
> alert !on,switch="porch light";
> alert on,switch="porch light";
2009/01/31 11:23:20 NB000I Rule TurnedOn fired (OnJust(switch)=1)
> alert !on,switch="porch light";
2009/01/31 11:23:20 NB000I Rule TurnedOff fired
: # The porch light turned off within 5 seconds of turning on
2009/01/31 11:23:20 NB000I Source file "./sequence.nb" included. size=453
2009/01/31 11:23:20 NB000I NodeBrain nb[6409] terminating - exit code=0
A cache node is useful for event correlation where the goal is to detect repetition, variation, or sequence. Rules can be used to recognize input events and assert multiple attribute combinations (tuples) to different cache nodes to detect different patterns. A single cache can detect multiple patterns, but it often necessary to specify attributes in a different order in different cache nodes to detect all the required patterns. For example, a cache specified as (child,action[5],toy) could detect a given child taking a given action on 5 different toys, while a cache specified as (toy[10],child[3],action(7)) could detect when 10 different children performed a given action on a given toy, a given child performed 3 different actions on a given toy, and a given child performed a given action on a given toy 7 times. The last condition could be detected by the first cache if we included another threshold, (child,action[5],toy(7)). But the first two conditions detected by the second cache could not be detected by the first cache. It is necessary to use two different cache nodes with the attributes in a different order to detect all 4 conditions.
In order to become the master, the politician poses as the servant. — Charles De Gaulle (1890 - 1970)
NodeBrain is not intended to be the master of all things. As in politics, it is often more convenient to let a servant be the master. In this tutorial you will learn how to create a servant in your favorite programming language to obtain information needed to make decisions. You will see that when a servant sends commands to NodeBrain, it becomes the master—like a politician once elected.
To keep it simple and only hint at something useful, let's create a servant script using Perl that pretends to tell us the cost of gas and bread, something every politician should be prepared to include in a campaign speech.
#!/usr/bin/perl
# File: tutorial/Servant/charles.pl
my $gas=2.50;
my $bread=1.10;
$|=1;
while(<>){
chomp($_);
if(/gas/){print("assert gas=$gas;\n");$gas+=.50;}
elsif(/bread/){print("assert bread=$bread;\n");$bread+=.25}
else{print("alert msg=\"item '$_' not recognized\";\n");}
}
Now we need some rules to use our servant program. Create a script that looks like this.
#!/usr/local/bin/nb -s
# File: tutorial/Servant/charles.nb
define price node servant:|=|:./charles.pl
define ouch on(gas>4 or bread>3):stop;
define getgasprice on(~(3s)):price:gas
define getbreadprice on(~(3s)):price:bread
The Servant node module specification includes an = command to specify the program and what to do with stdout and stderr.
It also supports a leading | to enable the sending of text to the program on stdin.
This script is designed to run like an agent without detaching from the terminal. The -s option is the trick. The script will pause for 3 seconds between scheduled events, so just be patient and the script will end when the price of one of the items gets too painful.
$ ./charles.nb
2008/08/21 19:08:28 NB000I Argument [2] ./charles.nb
> #!/usr/local/bin/nb -s
> # File: tutorial/Servant/charles.nb
> define price node servant:|=|:./charles.pl
> define ouch on(gas>4 or bread>3):stop;
> define getgasprice on(~(3s)):price:gas
> define getbreadprice on(~(3s)):price:bread
2008/08/21 19:08:28 NB000I Source file "./charles.nb" included. size=194
2008/08/21 19:08:28 NB000T Servant mode selected
---------- ----------------------------------------------------
2008/08/21 19:08:28 NM000I servant price: Enabling|=|:./charles.pl
2008/08/21 19:08:28 NM000I servant price: Enabled[21633] |=|:./charles.pl
2008/08/21 19:08:31 NB000I Rule getbreadprice fired
: price:bread
2008/08/21 19:08:31 NB000I Rule getgasprice fired
: price:gas
> price. assert bread=2.1;
> price. assert gas=3.25;
2008/08/21 19:08:34 NB000I Rule getbreadprice fired
: price:bread
2008/08/21 19:08:34 NB000I Rule getgasprice fired
: price:gas
> price. assert bread=2.35;
> price. assert gas=3.75;
2008/08/21 19:08:37 NB000I Rule getbreadprice fired
: price:bread
2008/08/21 19:08:37 NB000I Rule getgasprice fired
: price:gas
> price. assert bread=2.6;
> price. assert gas=4.25;
2008/08/21 19:08:37 NB000I Rule ouch fired
: stop;
2008/08/21 19:08:37 NB000I [21633] Killed(1)
2008/08/21 19:08:37 NB000I NodeBrain nb[21632] terminating - exit code=0
I don't mind what language an opera is sung in so long as it is a language I don't understand. — Sir Edward Appleton (1892 - 1965)
NodeBrain supports rules used to translate an opera into NodeBrain commands. Well, okay, not exactly an opera, but lines of text conforming to some foreign syntax for which the significant elements can be recognized and extracted with regular expressions. The desired translation is specified in a NodeBrain translation rule file called a "translator". This capability is made available to node modules via the API. The Translator node module is a simple example of one that uses NodeBrain's translator feature.
We'll start this tutorial with a file named opera.txt containing the text of a Mother Goose rhyme.
# File: tutorial/Translator/opera.txt
#
# Mother Goose Nursery Rhyme
# The Man Who Had Naught
#
There was a man and he had naught,
And robbers came to rob him;
He crept up to the chimney pot,
And then they thought they had him.
But he got down on t'other side,
And then they could not find him;
He ran fourteen miles in fifteen days,
And never looked behind him.
A translator recognizes elements of foreign text and converts it into NodeBrain commands. Let's create one called opera.nbx that can be used to translate files that look like our opera.txt file.
# File: tutorial/Translator/opera.nbx
#
# Ignore lines starting with "#"
(^#)
# Pick up lines with "had" followed by a word,
# but continue on looking for other matches
# even when a match is found.
@(had (\w*)):alert type="Had",what="$[1]";
# Look for "he had" lines - will be subset of "had" lines
(he had (\w*)):alert type="HeHad",what="$[1]";
# Look for lines starting with "And".
(^\s*And ){
(^then they ){
(thought)
:assert info="$[=]";
}
(^(\w*) )[$[1]]{
"robbers":assert info="bad guys $[>]";
"never":assert info="they didn't $[>]";
}
}
# Look for "He <verb>" lines.
(^He (crept|ran) ){
@"fourteen miles in fifteen days,":assert info="slow runner this man who had naught";
:assert heVerb="$[1]";
}
The lines starting with "(" specify a regular expression up to the balanced ")" to be matched against lines of foreign text. The first matching expression determines the translation; that is, the translator stops on a match and subsequent expressions are not evaluated. However, if you start the line with "@(" instead, the translator will continue even after a match.
If nothing follows the expression, as with "(^#)" and "(thought)" above, the translator takes no action on a match.
This means all lines starting with "#" will translate into nothing. You could say they are ignored or suppressed.
An expression can also be followed by a single action, like the @(had (\w*)) and (he had (\w*)) expressions which are followed by ":alert ...".
When an expression is followed by "{", a nested translator is specified up until the following "}".
A nested translator matches against the text following the previous match.
Let's consider the input line "And then they thought they had him."
When this line matches (^s*And ) in our translator,
the nested translation block will work on "then they thought they had him."
After a match on (^then they ), nested translation continues trying to match "thought they had him."
NodeBrain commands are passed to the interpreter with an expression starting with colon ":" to distinguish them from other operations supported by the translator. A $[n] in the command is replaced with the string matching the n'th parenthetical sub-expression, starting at 0 for the outer parentheses. A $[=] is replaced with the text being matched, and $[>] is replaced with the text following the last match.
The text buffer can be replaced with an expression of the form [text].
In our example above, the [$[1]] following (^(\w*) ) replaces the text buffer with the matched word.
Strings enclosed in double quotes (e.g. "robbers") specify an exact match. When strings are used, they must be placed at the start of a block. NodeBrain likes to look for exact matches before regular expressions.
A translator works in concert with NodeBrain rules—hopefully not as difficult to understand as an operatic concert.
Here's a rule file called opera.nb designed to work with our translator above.
#!/usr/local/bin/nb
# File: tutorial/Translator/opera.nb
define opera node translator("opera.nbx");
opera. define r1 on(info~"they didn't");
opera. define r2 if(type="HeHad");
opera:And robbers got away.
opera:And never mind.
opera:And never worry.
opera("translate"):opera.txt
The first highlighted line defines a translator node that uses our opera.nbx translator. Then we define two rules, r1 and r2, that do nothing except enable us to demonstrate that a rule can fire in response to foreign text.
The second highlighted line, which starts with "opera:", sends foreign text to the translator node for translation. The last highlighted line, starting with "opera(", directs the translator nodes to translate our foreign text file opera.txt.
When your execute opera.nb, you should see something like this.
$ ./opera.nb
2009/01/28 17:49:27 NB000I Argument [1] ./opera.nb
> #!/usr/local/bin/nb
> # File: tutorial/Translator/opera.nb
> define opera node translator("opera.nbx");
2009/01/28 17:49:27 NB000I Loading translator "opera.nbx"
---------- --------
# File: tutorial/Translator/opera.nbx
#
# Ignore lines starting with "#"
(^#)
# Pick up lines with "had" followed by a word,
# but continue on looking for other matches
# even when a match is found.
@(had (\w*)):alert type="Had",what="$[1]";
# Look for "he had" lines - will be subset of "had" lines
(he had (\w*)):alert type="HeHad",what="$[1]";
# Look for lines starting with "And".
(^\s*And ){
(^then they ){
(thought)
:assert info="$[=]";
}
(^(\w*) )[$[1]]{
"robbers":assert info="bad guys $[>]";
"never":assert info="they didn't $[>]";
}
}
# Look for "He <verb>" lines.
(^He (crept|ran) ){
@"fourteen miles in fifteen days,":assert info="slow runner this man who had naught";
:assert heVerb="$[1]";
}
---------- --------
2009/01/28 17:49:27 NB000I Translator "opera.nbx" loaded successfully.
> opera. define r1 on(info~"they didn't");
> opera. define r2 if(type="HeHad");
> opera:And robbers got away.
> opera. assert info="bad guys got away.";
> opera:And never mind.
> opera. assert info="they didn't mind.";
2009/01/28 17:49:27 NB000I Rule opera.r1 fired
> opera:And never worry.
> opera. assert info="they didn't worry.";
> opera("translate"):opera.txt
---------- --------> opera.txt
> opera. alert type="Had",what="naught";
> opera. alert type="HeHad",what="naught";
2009/01/28 17:49:27 NB000I Rule opera.r2 fired
> opera. assert info="bad guys came to rob him;";
> opera. assert heVerb="crept";
> opera. alert type="Had",what="him";
> opera. assert info="could not find him;";
> opera. assert info="slow runner this man who had naught";
> opera. assert heVerb="ran";
> opera. assert info="they didn't looked behind him.";
2009/01/28 17:49:27 NB000I Rule opera.r1 fired
---------- --------< opera.txt
2009/01/28 17:49:27 NB000I Source file "./opera.nb" included. size=237
2009/01/28 17:49:27 NB000I NodeBrain nb[16261] terminating - exit code=0
$
As an exercise, you should perform the translation yourself to make sure you understand what is going on here.
At least focus on the lines around the first firing of rule opera.r1, starting with "opera:And never mind."
Walk "And never mind." through the translator to see why it emits assert info="they didn't mind." triggering r1.
Do you understand why "opera:And never worry." emits a similar assertion without trigging r1?
It is a characteristic of NodeBrain's on rule and the fact that the condition didn't change.
An if rule would have fired both times.
We said that our translator opera.nbx is designed to operate on lines like those found in opera.txt, and we said that our rule file opera.nb is designed to work with our translator. You may have noticed that our translator was not designed specifically to work with our rules, because it emits commands that give us information our rules don't need. In many cases, a translator and a set of rules are designed together and the translator only emits information that is used by the rules.
If Edison had a needle to find in a haystack, he would proceed at once with the diligence of the bee to examine straw after straw until he found the object of his search... I was a sorry witness of such doings, knowing that a little theory and calculation would have saved him ninety per cent of his labor. — Nikola Tesla (1857- 1943), New York Times, October 19, 1931
Effective review of system and application logs can be like trying to find a needle in a haystack. It requires at least one Edison and one Tesla working as a team. The Audit node module works like Edison when reviewing logs so you can work like Tesla.
An Audit node is similar to the Translator node covered in an earlier tutorial, but differs in the way lines of text are input for translation. An Audit node starts at the end of a log file and periodically checks for new lines to translate. When a log file rolls, the audit node starts at the beginning of the new log file.
The content of system and application log files can vary significantly depending on the mix of applications on a system and how they are configured. A good strategy is to treat log entries as worthy of investigation by default. Duplicate suppression and other flood protection techniques are helpful when using this strategy. As new log entries are reported and investigated, you can decide if they should be suppressed or handled in a special way.
The syslog.nb file below provides an agent with an audit node to monitor a log file called syslog. It specifies a translator named syslog.nbx and a polling interval of 10 seconds. (A longer interval is recommended for real application.) We have also included a deduplication cache.
#!/usr/local/bin/nb -d
# File: tutorial/Audit/syslog.nb
set log="syslog.log",out=".";
define syslog node cache(~(h(8))):(~(1h):route,appl,group,node,object,severity,text(1));
syslog. define alarm if(text._hitState):$ -|mail.form source=tutorial route="${route}" appl="${appl}" group="${group}" node="${node}" severity="${severity}" text="${text}" >> mail.log
syslog. define audit node audit("syslog","syslog.nbx",~(10s));
Here's a small sample of a log file in the format we'll use for this tutorial. A copy of this file is stored as tutorial/Audit/syslog.sample.
Feb 1 19:00:04 smk001 sshd[3972]: Accepted publickey for myuser from ::ffff:192.168.1.100 port 53403 ssh2
Feb 1 19:00:06 smk001 sshd[3980]: Accepted publickey for myuser from ::ffff:192.168.1.101 port 53410 ssh2
Feb 1 19:00:16 smk001 kernel: z90crypt: probe_crypto_domain -> Unable to find crypto domain: No devices found
Feb 1 19:00:46 smk001 kernel: z90crypt: probe_crypto_domain -> Unable to find crypto domain: No devices found
Feb 1 19:01:16 smk001 kernel: z90crypt: probe_crypto_domain -> Unable to find crypto domain: No devices found
Feb 1 19:01:19 smk001 su: (to root) myuser on /dev/pts/1
Here's a small translator called syslog.nbx designed for the log format above.
# File: tutorial/Audit/syslog.nbx
([a-zA-Z]+ +\d+ \d\d:\d\d:\d\d [^ ]+ ){
(^-- MARK --)
(^\/USR\/SBIN\/CRON\[\d+\]: [^ ]+ CMD)
(^last message repeated \d+ times)
(^kernel: ){
(^z90crypt: probe_crypto_domain -> Unable to find crypto domain: No devices found)
(^end_request: I\/O error)
(^dasd_erp.*:Perform logging requested)
(^dasd:.*:ERP successful)
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 kernel: $[=]");
}
(^su: ){
(^pam_unix2: session (started|finished) for user (nobody|root|wwwadm|cyrus), service su)
(^\(to (nobody|cyrus)\) root on none)
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 su: $[=]");
}
(^sshd\[\d+\]: ){
(^Accepted password for myuser from ::ffff:.* port \d+ ssh2)
(^Accepted publickey for myuser from ::ffff:.* port \d+ ssh2)
(^error: Could not get shadow information for NOUSER)
(^(?'preport'.*port) \d+ ){
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 sshd[*]: $[preport]port * $[=]");
}
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 sshd[*]: $[=]");
}
}
():syslog. assert ("syslog","syslog","OS","","","critical","SYS0000 $[-]");
Before starting the agent you must touch a file named syslog to make sure it exists.
$ ./syslog.nb
2009/02/01 19:56:57 NB000I Argument [1] -d
2009/02/01 19:56:57 NB000I Argument [2] ./syslog.nb
> #!/usr/local/bin/nb -d
> # File: tutorial/Audit/syslog.nb
> set log="syslog.log",out=".";
2009/02/01 19:56:57 NB000I NodeBrain nb will log to syslog.log
> define syslog node cache(~(h(8))):(~(1h):route,appl,group,node,object,severity,text(1));
> syslog. define alarm if(text._hitState):$ -|mail.form source=tutorial route="${route}" appl="${appl}" group="${group}" node="${node}" severity="${severity}" text="${text}" >> mail.log
> syslog. define audit node audit("syslog","syslog.nbx",~(10s));
2009/02/01 19:56:57 NB000I Loading translator "syslog.nbx"
---------- --------
# File: tutorial/Audit/syslog.nbx
([a-zA-Z]+ +\d+ \d\d:\d\d:\d\d [^ ]+ ){
(^-- MARK --)
(^\/USR\/SBIN\/CRON\[\d+\]: [^ ]+ CMD)
(^last message repeated \d+ times)
(^kernel: ){
(^z90crypt: probe_crypto_domain -> Unable to find crypto domain: No devices found)
(^end_request: I\/O error)
(^dasd_erp.*:Perform logging requested)
(^dasd:.*:ERP successful)
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 kernel: $[=]");
}
(^su: ){
(^pam_unix2: session (started|finished) for user (nobody|root|wwwadm|cyrus), service su)
(^\(to (nobody|cyrus)\) root on none)
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 su: $[=]");
}
(^sshd\[\d+\]: ){
(^Accepted password for myuser from ::ffff:.* port \d+ ssh2)
(^Accepted publickey for myuser from ::ffff:.* port \d+ ssh2)
(^error: Could not get shadow information for NOUSER)
(^(?'preport'.*port) \d+ ){
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 sshd[*]: $[preport]port * $[=]");
}
:syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 sshd[*]: $[=]");
}
}
():syslog. assert ("syslog","syslog","OS","","","critical","SYS0000 $[-]");
---------- --------
2009/02/01 19:56:57 NB000I Translator "syslog.nbx" loaded successfully.
2009/02/01 19:56:57 NB000I Source file "./syslog.nb" included. size=425
2009/02/01 19:56:57 NB000I NodeBrain nb[6749,3352] daemonizing
$
We'll just use a shell script to append our sample log file syslog.sample to the monitored syslog file three times.
# File: tutorial/Audit/syslog.sh
cat syslog.sample >> syslog
cat syslog.sample >> syslog
cat syslog.sample >> syslog
After executing the syslog.sh script above, your syslog.log file should look like this.
2009/02/01 19:59:35 NB000I Agent log is syslog.log
2009/02/01 19:59:35 NM000I audit audit: Enabled audit of syslog using syslog.nbx
> syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 su: (to root) myuser on /dev/pts/1");
2009/02/01 20:00:05 NB000I Rule syslog.alarm fired
: syslog. -|mail.form source=tutorial route="syslog" appl="syslog" group="OS" node="" severity="normal" text="SYS0000 su: (to root) myuser on /dev/pts/1" >> mail.log
[6957] Started: -|mail.form source=tutorial route="syslog" appl="syslog" group="OS" node="" severity="normal" text="SYS0000 su: (to root) myuser on /dev/pts/1" >> mail.log
[6957] Exit(0)
> syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 su: (to root) myuser on /dev/pts/1");
> syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 su: (to root) myuser on /dev/pts/1");
> syslog. assert ("syslog","syslog","OS","","","normal","SYS0000 su: (to root) myuser on /dev/pts/1");
Notice that the translator only asserted the su log entries to the syslog cache. This is because our translator was coded to ignore the other entries in our sample log file. Notice also that the agent only generated an alarm once. This is because the syslog cache node was coded to ignore duplicates until 8:00 AM unless they are separated by 1 hour.
Although we used a system log for this tutorial, you can code up a translator for application logs as well. In fact, the more unique your application log, the more likely you will need to construct your own custom log monitor with one of the many tools like this.
If you started the agent for this tutorial, you should kill it now.
I decided that it was not wisdom that enabled [poets] to write their poetry, but a kind of instinct or inspiration, such as you find in seers and prophets who deliver all their sublime messages without knowing in the least what they mean. — Socrates (469 BC - 399 BC), In "Apology," sct. 21, by Plato.
If your sublime messages are delivered via the syslog protocol, you may prefer to use a syslog node instead of an audit node. This enables NodeBrain to respond immediately to arriving syslog UDP packets without waiting to poll a log file.
You have a couple options. You can configure your syslog node to listen on UDP port 514 on a server that doesn't already have a syslog daemon. However, if you need (or want) more flexibility, we recommend that you use NodeBrain in combination with syslog-ng. In that case, you configure NodeBrain to listen on a different port and configure syslog-ng to forward all or selected syslog entries to NodeBrain.
The example below is configure to listen on UDP port 1514, assuming syslog-ng is used to forward syslog to NodeBrain.
#!/usr/local/bin/nb -d
# File: tutorial/Syslog/syslog.nb
-rm syslog.log
set log="syslog.log",out=".";
define syslog node cache(~(h(8))):(~(1h):route,appl,group,node,object,severity,text(1));
syslog. define alarm if(text._hitState):$ -|mail.form source=tutorial route="${route}" appl="${appl}" group="${group}" node="${node}" severity="${severity}" text="${text}" >> mail.log
syslog. define audit node syslog("syslog.nbx",1514);
You should reference the documentation for syslog-ng to see how to configure it to forward to NodeBrain. Here's an example to get you started.
destination nodebrain { udp("localhost" port(1514)); };
filter f_nodebrain { host("(humpty|dumpty).mydomain.com|franklin.otherdomain.com"); };
log { source(src); filter(f_nodebrain); destination(nodebrain); };
Refer to the Audit Node tutorial above for a sample syslog.nbx file. Refer to the Translator Node tutorial for more information on coding a translator.
Man is the only kind of varmint who sets his own trap, baits it, then steps on it. — John Steinbeck (1902 - 1968)The snmptrap node is used to monitor SNMP traps. This is one method of configuring NodeBrain to accept alerts from monitoring tools that are capable of sending SNMP traps. NodeBrain's snmptrap node is a bit unusual in that it does not use MIB's. Instead, each trap is converted into a NodeBrain alert using single quoted OID terms.
alert 'oid'="value",'oid'="value",...;
You must then code your rules referencing the 'oid' terms. However, we recommend that you define aliases for the OID's of interest to make your rules more readable.
#!/usr/local/bin/nb -d
# File: tutorial/Snmptrap/snmptrap.nb
-rm snmptrap.log
set log="snmptrap.log",out=".";
# Node
define snmptrap node snmptrap:trace,dump;
# Aliases
snmptrap. define myProduct cell '1.3.6.1.6.3.1.1.4.3'="1.3.6.1.4.1.1279";
snmptrap. define address cell '1.3.6.1.4.1.1279.4';
snmptrap. define type cell '1.3.6.1.4.1.1279.5';
# Rules
snmptrap. define r1 if(myProduct and type="hiccup");
The example above is only provided to illustrate the syntax for working with single quoted OID terms. You will need to adapt this example to the traps you want to monitor to construct useful rules. However, you can use this example to start collecting traps right away. The traps will show up in your snmptrap.log file. Then you can figure out what you want to monitor.
Remove the ":trace,dump" from your snmptrap node specification to reduce the amount of information in your log.
define snmptrap node snmptrap;
You may use a "silent" option to stop logging the alerts generated by the snmptrap node.
define snmptrap node snmptrap:silent;
To listen closely and reply well is the highest perfection we are able to attain in the art of conversation. — Francois de La Rochefoucauld (1613 - 1680)
To construct a successful NodeBrain application we must configure NodeBrain to listen closely and respond well. The "Operating Mode Tutorial" illustrates how NodeBrain can listen for input commands from stdin and both input commands and error messages from servant scripts. NodeBrain's ability to listen is controlled by a component called the "medulla". Node modules interface with the medulla to extend NodeBrain's ability to listen to include other sources of input.
A pipe node is perhaps the simplest of listening nodes. It listens to a FIFO (named pipe) file, and you can write to the
pipe using any program you like, including an echo command.
We begin this tutorial by creating an agent script called smokey.nb with two pipe server nodes as shown here.
#!/usr/local/bin/nb -d
# File: tutorial/Pipe/smokey.nb
-rm smokey.log
setlog="smokey.log",out=".";
declare jed identity guest;
declare chief identity owner;
define corncob node pipe.server("jed@corncob");
corncob.define r1 on(a=1 and b=2);
define peace node pipe.server("chief@peace");
peace.define r1 on(a=1 and b=2);
The argument to a pipe.server is of the form "identity@pipe".
Identities are associated with listening nodes to limit the types of commands the interpreter will accept from the node. In this example we associate the identity "jed" with the corncob pipe, and the identity "chief" with the peace pipe.
We have declared jed to be a guest and chief to be an owner. A guest can only connect and issue show commands—like read only access.
An owner can issue any command, including shell commands, which means they have all permissions of the user that started the agent. For this reason, pipes are created with owner only read/write permissions.
However, if you declare the associated identity to be a "peer", you can give other user's write permission on the pipe.
They will be able to issue assertions and alerts, but not modify your rules or issue shell commands.
You must still think through how the agent will respond to their assertions.
For example, if you create a rule that reboots the system when a=1, then letting someone assert a=1 is the same as letting them reboot the system.
Now let's start our agent. We've made this script executable and specified the -d (daemon) option on the she-bang line. So we can just execute it like any executable and it will load the rules and go into the background (daemonize).
$ ./smokey.nb
2008/06/1017:09:16 NB000I Argument [1] -d
2008/06/1017:09:16 NB000I Argument [2] ./smokey.nb
> #!/usr/local/bin/nb -d
> # File: smokey.nb
> -rm smokey.log
[13993]Started: -rm smokey.log
[13993]Exit(0)
> set log="smokey.log",out=".";
2008/06/1017:09:16 NB000I NodeBrain nb will log to smokey.log
> declare jed identity guest;
> declare chief identity owner;
> define corncob node pipe.server("jed@corncob");
> corncob. define r1 on(a=1 and b=2);
> define peace node pipe.server("chief@peace");
> peace. define r1 on(a=1 and b=2);
2008/06/1017:09:16 NB000I Source file "./smokey.nb" included. size=323
2008/06/1017:09:16 NB000I NodeBrain nb[13992,4118] daemonizing
$
We'll use the echo command to send NodeBrain commands to our pipe servers.
$ echo stop > corncob
$ echo stop > peace
The log file shows us what happened.
$ cat smokey.log
N o d e B r a i n 0.7.0(Dunce) 2008/05/24
Compiled Jun 10 2008 15:30:28s390x-ibm-linux-gnu
Copyright (C) 1998-2008 The Boeing Company
GNU General Public License
----------------------------------------------------------------
/usr/local/bin/nb -d ./smokey.nb
Date Time Message
---------- ----------------------------------------------------
2008/06/10 17:09:16 NB000I NodeBrain nb[13994:1] myuser@myhost
2008/06/10 17:09:16 NB000I Agent log is smokey.log
2008/06/10 17:09:16 NM000I pipe.server peace: Listening for FIFO connections as chief@peace
2008/06/10 17:09:16 NM000I pipe.server corncob: Listening for FIFO connections as jed@corncob
2008/06/10 17:41:11 NM000I pipe.server corncob: FIFO jed@corncob
> corncob. stop
2008/06/10 17:41:11 NB000E Identity "jed" does not have authority to issue stop command.
2008/06/10 17:41:19 NM000I pipe.server peace: FIFO chief@peace
> peace. stop
2008/06/10 17:41:19 NB000I NodeBrain nb[13994] terminating - exit code=0
$
Notice that when we sent a stop command to the corncob pipe we didn't have the needed authority, but when we sent the same stop command to the peace pipe it worked.
Run smokey.nb again with the following commands to see what happens.
$ ./smokey.nb
$ echo "common. define r1 on(a=1 and b=2);" > peace
$ echo "common. assert a=1,b=3;" > peace
$ echo "common. assert b=2;" > peace
$ echo "common. show -t" > peace
$ echo "stop" > peace
$ cat smokey.log
The more elaborate our means of communication, the less we communicate. — Edwin Schlossberg
The peer module is more elaborate than some of the other modules used for communication. The goal is to communicate less, or at least to be more selective in with whom we communicate. This selectivity is accomplished with encryption and key based authentication.
The files for this tutorial are in the tutorial/Peer directory. The NodeBrain scripts are executable and we have left the .nb suffix off for fun.
Before we communicate using the peer module, we must create keys to be used by clients and servers. To reduce complexity for this example, we'll create a single key and use it for both the client and the server as a "shared secret" key. Here's a script called genkey that creates a key for an identity called buddy.
#!/usr/local/bin/nb
# File: tutorial/Peer/genkey
define myService node peer.service;
myService:identify buddy;
The peer module provides a skill called service that supports some helpful commands associated with peer communication.
We are using the identify command here.
This command generates a key and places it in a key store for later reference.
| Unix and Linux: | ~/.nb/nb_peer.keys
|
| Windows: | USER_PROFILE/ApplicationData/NodeBrain/nb_peer.keys
|
The key store is readable only by the owning user and looks like this.
$ cat ~/.nb/nb_peer.keys
foo 3.3788e45e8f64776b.0.0;
bar 7.b49ad9bfb68a97b8.fab67908064b5cb3.0;
buddy 3.be5d8bc4465d3aa7.bd6107c786f72c15.0;
Be alert to give service. What counts a great deal in life is what we do for others. — Anonymous
A peer server node is alert to give service, if fact, it can even give service to alerts.
It provides a method for a peer client to send any command to a NodeBrain agent: alerts, assertions, new rule definitions, etc.
It accepts TCP/IP socket connections from clients and issues received commands in the context of the server node. The server file in the tutorial/Peer directory looks like this.
#!/usr/local/bin/nb -d
# File: tutorial/Peer/server
-rm server.log
set out=".",log="server.log";
declare buddy identity owner;
define myServer node peer.server("buddy@./socket"); # Unix domain
#define myServer node peer.server("buddy@127.0.0.1:12345"); # local
#define myServer node peer.server("buddy@0.0.0.0:12345"); # remote
myServer. define r1 on(a=1 and b=2);
We declare an identity buddy that we rank as owner. This is all the interpreter knows about buddy. We use this same name, buddy, in the specification of the peer server node. The peer node module requires that a key exist in the key store for this identity, which is why we created it in the previous section.
Let's start up our peer server.
$ ./server
2008/06/11 10:27:58 NB000I Argument [1] -d
2008/06/11 10:27:58 NB000I Argument [2] ./server
> #!/usr/local/bin/nb -d
> # File: tutorial/Peer/server
> -rm server.log
[20642] Started: -rm server.log
[20642] Exit(0)
> setout=".",log="server.log";
2008/06/11 10:27:58 NB000I NodeBrain nb will log to server.log
> declare buddy identity owner;
> define myServer nodepeer.server("buddy@./socket"); # Unix domain
2008/06/11 10:27:58 NB000I Peer keys loaded.
> #define myServer nodepeer.server("buddy@127.0.0.1:12345"); # local
> #define myServer node peer.server("buddy@0.0.0.0:12345"); # remote
> myServer. define r1 on(a=1 and b=2);
2008/06/11 10:27:58 NB000I Source file "./server" included. size=493
2008/06/11 10:27:58 NB000I NodeBrain nb[20641,19542]daemonizing
$
We are using a Unix domain socket in this tutorial because we don't need to communicate with remote clients. To experiment with serving remote clients, you can comment out the active myServernode and uncomment the remote myServer node. But first you should experiment with the client in the next section
He who is always his own counselor will often have a fool for his client. — Hunter
The peer module enables NodeBrain to play the part of both server and client. This is not foolish because we are talking about different instances of NodeBrain, different processes ("skulls"), playing the roles of client and server.
The client file in the tutorial/Peer directory looks like this.
#!/usr/local/bin/nb
# File: tutorial/Peer/client
declare buddy identity;
define myClient nodepeer.client("buddy@./socket"); # Unix domain
#define myClient nodepeer.client("buddy@localhost:12345"); # local
#define myClient nodepeer.client("buddy@myhost.mydomain:12345"); # remote
myClient:assert a=1,b=2;
myClient:stop;
Notice we declare the identity buddy and specify the client just like we specified the server in the previous section. Here there is no requirement to give buddy local permissions.
Because we are going to run this client on the same machine as our server, and under the same user account, the client and server will use the same key store.
To run the client from a different account or machine, we would have to copy the buddy key from the server's key store to the client's key store.
We could also configure different private keys for servers and clients and copy their public keys to the key stores of their peers. That approach provides better security, but we're more interested in quick success in this tutorial, so we'll stick with shared secret keys.
Now we can run our client and see what happens.
$ ./client
2008/06/11 10:28:00 NB000I Argument [1] ./client
> #!/usr/local/bin/nb
> # File: tutorial/Peer/client
> declare buddy identity;
> define myClient nodepeer.client("buddy@./socket"); # Unix domain
> #define myClient nodepeer.client("buddy@localhost:12345"); # local
> #define myClient nodepeer.client("buddy@myhost.mydomain:12345"); # remote
> myClient:assert a=1,b=2;
2008/06/11 10:28:00 NB000I Peer keys loaded.
2008/06/11 10:28:00 NB000I Peer b0000=buddy@./socket
2008/06/11 10:28:00 NB000I Rule myServer.r1 fired
> myClient:stop;
2008/06/11 10:28:00 NB000I Peer b0000=buddy@./socket
2008/06/11 10:28:00 NB000I Source file "./client" included. size=450
2008/06/11 10:28:00 NB000I NodeBrain nb[20644] terminating- exit code=0
$
The command myClient:assert a=1,b=2 sends the command assert a=1,b=2 to the server specified as buddy@./socket. Notice the message Rule myServer.r1 fired.
There is no myServer.r1 defined in the client. This is what happened at the server. When a peer client issues a command to a peer server, the server lets the client listen in as it reacts to the command. We can look at it from the server's point of view by displaying the agent log, in this case called server.log.
$ cat server.log
N o d e B r a i n 0.7.0(Dunce) 2008/05/24
Compiled Jun 10 2008 15:30:28s390x-ibm-linux-gnu
Copyright (C) 1998-2008 The Boeing Company
GNU General Public License
----------------------------------------------------------------
nb -d ./server
Date Time Message
---------- ----------------------------------------------------
2008/06/11 10:27:58 NB000I NodeBrain nb[20643:1] myuser@myhost
2008/06/11 10:27:58 NB000I Agent log is server.log
2008/06/11 10:27:58 NM000I peer.server myServer: Listening for NBP connections as buddy@./socket
2008/06/11 10:28:00 NM000I peer.server myServer: buddy@./socket
> myServer. assert a=1,b=2;
2008/06/11 10:28:00 NB000I RulemyServer.r1 fired
2008/06/11 10:28:00 NM000I peer.server myServer: buddy@./socket
> myServer. stop;
2008/06/11 10:28:00 NB000I NodeBrain nb[20643] terminating - exit code=0
$
At 10:27:58 our server started listening for connections. At 10:28 we received a connection and issued the command assert a=1,b=2 in the myServer context. This triggered rule myServer.r1 which has no action. In previous tutorials you've learned enough to modify the rules in server to provide an action and try it again.
Notice our client sends a stop command to the server, which stops it because we've given the client full owner permissions. This means we have to restart the server each time we run the client. Only in a tutorial would we do something this silly.
If you want to experiment further with the peer module, start the server again and try the iclient file instead of client.
$ ./server
$ ./iclient
The iclient script looks like this.
#!/usr/local/bin/nb -`myClient:
# File: iclient
declare buddy identity;
define myClient nodepeer.client("buddy@./socket"); # Unix domain
The strange looking she-bang line has an argument that supplies an interactive command prefix and enables an option to automatically go into interactive mode after processing all command arguments. This causes your prompt to look like this.
myClient:>
Any command you enter is now prefixed by the value myClient:, causing all your commands to be directed to your peer client node, which sends them to your peer server.
Enter the highlighted text when prompted to get the same results as shown below.
$ ./iclient
2008/06/11 16:48:39 NB000I Argument [1] -'myClient:
> #!/usr/local/bin/nb -'myClient:
> # File: tutorial/Peer/iclient
> declare buddy identity;
> define myClient nodepeer.client("buddy@./socket"); # Unix domain
2008/06/11 16:48:39 NB000I Source file "./iclient" included. size=209
2008/06/11 16:48:39 NB000I Reading from standard input.
---------- --------
myClient:> show r1
2008/06/11 16:48:45 NB000I Peer keys loaded.
2008/06/11 16:48:45 NB000I Peer b0000=buddy@./socket
> myServer. show r1
r1 = ! == on((a=1)& (b=2));
myClient:> assert a=1,b=2;
2008/06/11 16:49:10 NB000I Peer b0000=buddy@./socket
> myServer. assert a=1,b=2;
2008/06/11 16:49:10 NB000I Rule myServer.r1 fired
myClient:> 'foo.
foo.> '
> quit
2008/06/11 16:49:15 NB000I NodeBrain nb[23715] terminating - exit code=0
$
This example illustrates how a single quote at the beginning of an interactive command can be used to change the command prefix.
The little tricks we illustrated in this section are features of the interpreter, not the peer module, but when combined with the peer module make it a bit easier to use NodeBrain as a primitive interactive client to a NodeBrain agent.
The world is governed more by appearance than realities, so that it is fully as necessary to seem to know something as to know it. — Daniel Webster (1782 - 1852)
The Webster module enables NodeBrain to pretend to be a web server. Not with the goal of providing a web server for web applications in general, but to support little web tools associated with NodeBrain applications.
Although Webster supports x509 certificate authentication, let's skip over all that fun stuff and get it running quickly with no security. If we don't secure it with password or certificate authentication, Webster doesn't let us do anything other than display web pages, so other than exposing a port that could be vulnerable to buffer overflow errors if we have bugs, there is no risk in running it without security.
For this tutorial our files are in the tutorial/Webster sub-directory of the distribution directory. The server script webster1 looks like this.
#!/usr/local/bin/nb -d
# File: tutorial/Webster/webster1
-rm webster.log
set out=".",log="webster.log";
define webster node webster("default@0.0.0.0:62443");
webster. define Protocol cell "HTTP";# Default is "HTTPS"
webster. define Authenticate cell "no"; # Default is "yes"
webster. define DocumentRoot cell "web"; # Default is "web"
$
A Webster server is specified as identity@interface:port.
We are using the "default" identity, all interfaces "0.0.0.0", and port 62443. The "443" reminds us that we'd prefer to use HTTPS in a real application. Change the port number if 62443 is used on your system.
If there are no errors in the log file, you can leave your server running and connect with your web browser using the following URL.
http://hostname:62443
This tutorial continues on the page displayed by your browser.
Copyright © 2000,2001,2002 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
The purpose of this License is to make a manual, textbook, or other functional and useful document free in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.
This License is a kind of “copyleft”, which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.
We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.
This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The “Document”, below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as “you”. You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.
A “Modified Version” of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.
A “Secondary Section” is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.
The “Invariant Sections” are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.
The “Cover Texts” are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.
A “Transparent” copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not “Transparent” is called “Opaque”.
Examples of suitable formats for Transparent copies include plain ascii without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.
The “Title Page” means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, “Title Page” means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.
A section “Entitled XYZ” means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as “Acknowledgements”, “Dedications”, “Endorsements”, or “History”.) To “Preserve the Title” of such a section when you modify the Document means that it remains a section “Entitled XYZ” according to this definition.
The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.
You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.
You may also lend copies, under the same conditions stated above, and you may publicly display copies.
If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.
If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.
If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.
It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.
You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:
If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.
You may add a section Entitled “Endorsements”, provided it contains nothing but endorsements of your Modified Version by various parties—for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.
You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.
You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.
The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.
In the combination, you must combine any sections Entitled “History” in the various original documents, forming one section Entitled “History”; likewise combine any sections Entitled “Acknowledgements”, and any sections Entitled “Dedications”. You must delete all sections Entitled “Endorsements.”
You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.
You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.
A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an “aggregate” if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.
If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.
Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.
If a section in the Document is Entitled “Acknowledgements”, “Dedications”, or “History”, the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.
You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.
Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License “or any later version” applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.
To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:
Copyright (C) year your name.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.2
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
Texts. A copy of the license is included in the section entitled ``GNU
Free Documentation License''.
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the “with...Texts.” line with this:
with the Invariant Sections being list their titles, with
the Front-Cover Texts being list, and with the Back-Cover Texts
being list.
If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.
If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.