Constraints

Constraints are a way of specifying valid relationships among parameters to nodes and links. The constraints system is designed to act as a broad brush outlining what is possible. It can be used as a 'beginner mode' with a few rules describing a subset that is almost certainly possible. Or it can be used to provide an 'advanced mode' providing a superset of what is possible using just a few rules. The important point is few rules. This won't scale to the number of rules and complexity that would be required to provide a truly accurate map of the rspecs which will work.

Theory

The basic unit of Jacks constraints is the clause. Each clause defines a valid mapping between the cartesian product of some set of values of particular types. Each clause is implicitly combined with all the other clauses that span the same set of types into a single group. Groups form a single whitelist of valid mappings among the types of the group. If any of the clauses in the group match, then the group as a whole matches.

Thus, the overall structure of the constraints is a boolean product of sums:

GroupA && GroupB && ... && GroupZ

GroupA = Clause1 || Clause2 || Clause3 ... ClauseN
GroupB = ClauseN+1 || ClauseN+2 || ... CLauseN+M
GroupC = ...

Clauses

Each clause is a key/value mapping where each key is one of a fixed number of value types (one each for node type, link type, image id, etc.), and the value is a list of ids from the canvasOptions for that type. For convenience, these keys are subdivided into node/link/node2 sections which are described in the Practice section below. For these examples, everything is implicitly in the 'node' section.

The set of valid combinations between the key types is the cartesian product among the list values.

types: ['a'],
images: ['imageFoo', 'imageBar']

/* Allows nodes of (type, image) of
   ('a', 'imageFoo'), ('a', 'imageBar') */

There can be more than two keys in the cartesian product of allowed combinations:

types: ['a', 'b'],
images: ['imageFoo', 'imageBar'],
hardware: ['pc600', 'pc850']

/* Allows nodes of (type, image, hardware) of
   ('a', 'imageFoo', 'pc600'), ('a', 'imageFoo', 'pc850'),
   ('a', 'imageBar', 'pc600'), ('a', 'imageBar', 'pc850'),
   ('b', 'imageFoo', 'pc600'), ('b', 'imageFoo', 'pc850'),
   ('b', 'imageBar', 'pc600'), ('b', 'imageBar', 'pc850') */

A wildcard of '*' means that any option for this type specified in canvasOptions is allowed. Suppose types 'a', 'b', and 'c' were specified in canvasOptions:

types: ['*'],
images: ['imageFoo']

/* Allows nodes of (type, image) of
   ('a', 'imageFoo'), ('b', 'imageFoo'), ('c', 'imageFoo') */

Candidate combinations consist of a set of properties, some of which are bound and some of which are unbound (unselected). Unbound property types also act as a kind of wildcard, matching any list of values for that type.

types: ['a'],
images: ['imageFoo']

/* Allows nodes of (type, image) of
   ('a', 'imageFoo'), (undefined, 'imageFoo'),
   ('a', undefined), (undefined, undefined)

Groups

Clauses are implicitly combined into groups. Groups act as a single combined whitelist. Each unique set of keys forms a different group, even if one group has a subset of the keys in a nother group.

Suppose, one group uses the keys (types, images) and another uses the keys (images, hardware) and a third uses the keys (types, images, hardware). Each of these three groups form an independant whitelist. And every group must be matched. A candidate which matches the first two groups but does not match the third group will be rejected.

Any group that does not have any clauses does not constrain that particular set of keys. Thus, an empty constraints list means that any combination of properties is valid. As soon as a group contains at least one clause, that group is now constrained to the whitelist formed by its clauses. This means that adding a single clause can constrain the combinations much more than you might expect if that clause forms a new group.

It is also important to note that groups can constrain individual selections in surprising ways. Suppose that types 'a', 'b', and 'c' are defined in canvasOptions and there was a group with two clauses:

types: ['a'],
hardware: ['pc600']

OR

types: ['b'],
hardware: ['pc850']

In the above case, the user will never able to select a node type of 'c', even if 'hardware' is unselected/undefined, and even though it was defined in canvasOptions. This is because there is no hardware type that it can be paired with.

Practice

The constraints structure passed to Jacks is a flat list inside of the main options object:

var instance = new window.Jacks({
  mode: 'viewer',
  source: 'rspec',
  root: '#jacksContainer',
  canvasOptions: { ... },
  constraints: [ ... ]});

Each item in the list is a clause. For convenience the keys in a clause are divided into three sections, each of which is optional.

[
  {
    node: {
      types: ['a']
    },
    link: {
      linkTypes: ['tunnel']
    }
    node2: {
      types: ['a']
    }
  },
  {
    ...
  },
  ...
]

The node section is for constraining individual nodes. It can also be combined with the link and node2 sections to constrain a link and pairwise nodes incident upon that link. The details of how these sections are mapped to the actual topology are described below.

Clauses are implicitly combined into groups and evaluation of candidates against these clauses is described above. These three sections of keys are all evaluated as a single unit. The only exception is that nodes without incident links are not evaluated against clauses with link sections.

Clauses with only a node and a link section implicitly add a clause with node, link, node2 sections with the node2 section identical to the node section.

Clauses with a different node and node2 sections implicitly add a clause with node and node2 sections swapped.

Mapping Topologies

When determining validity, we need to generate candidates from the topology to evaluate against the clauses. The mapping of the topology to these candidates determines what kinds of things can be constrained.

Sections and Keys

In the node and node2 sections, keys are generated for types, hardware, and images which correspond to those pieces of canvasOptions.

In the link section, keys are generated for linkTypes which corresponds to that piece of canvasOptions.

All keys are undefined if not selected. Undefined keys act as wildcards and are accepted for that part of any clause.

Candidates

Every node generates a candidate with only a node section which is evaluated only against clauses without a link section.

Every node with incident links generates a candidate for every other node incident upon those links with node (for the current node), link, and `node2 (for the node on the other side of the link) section. This means that node constraints can be asymmetrical across a link. And it also means that only pairwise constraints on adjacent nodes can be applied. There is no way to simultaneously constrain three different adjacent nodes in one clause even if they are on the same LAN.

Every link generates a candidate for every pair of nodes incident upon that link. The candidate has node, link, and node2 sections. The assignment of which endpoint is node and which is node2 is arbitrary (hence the implicit clauses added above)

Examples

Below are some example clauses and descriptions of what they constrain.

[
...
{
  node: {
    'hardware': ['d710', 'pc3000'],
    'types': ['emulab-rawpc', 'emulab-xen']
  }
},
{
  node: {
    'hardware': ['pc3000'],
    'types': ['*']
  }
},
{
  node: {
   'hardware': ['*'],
   'types': ['default-vm']
  }
},
...
]

These three clauses together form a group to constrain the relationship between 'hardware' and 'types' for every node whether it has links or not. The 'pc3000' harware type works with any node type. The 'default-vm' node type works with any hardware type. And both the 'd710' and 'pc3000' hardware work with either the 'emulab-rawpc' or 'emulab-xen' node types. Note that there is some overlap here between the clauses, and this is perfectly fine because they group together to form a whitelist.

[
...
{
  node: {
   'types': ['emulab-xen']
  },
  link: {
   'linkTypes': ['egre-tunnel']
  },
  node2: {
   'types': ['emulab-xen']
  }
},
{
  node: {
   'types': ['emulab-rawpc']
  },
  link: {
   'linkTypes': ['stitched'],
  },
  node2: {
   'types': ['m1.small']
  }
},
...
]

The two clauses above combine together to constrain types and linkTypes on adjacent pairs of nodes connected by links. They do not apply to nodes with no incident links. The first clause allows an 'egre-tunnel' link type only between pairs of nodes with the 'emulab-xen' type. In the first clause, technically the 'node2' section is superfluous since the 'node' section is identical to it. The second clause means that 'stitched' link types can only exist between pairs of nodes where one side is of type 'emulab-rawpc' and the other is of type 'm1.small'. That means that you cannot have a two nodes with 'm1.small' connected via a 'stitched' link.

If these are the only two clauses in this group, it will constrain the node types of any node with incident links, not just those whose node type is one of the two specified. For example, if a link with an unselected link type is incident upon a node with an 'm1.small', the other node will only list the 'emulab-rawpc' node type as an option. If the other node is also incident upon a link to a node with an 'emulab-xen' link type, then there are no node types that can fulfill both of these clauses and so no possible selections will be shown.