RFC 1076

HEMS monitoring and control language

๐Ÿ“… published November 1988, from ietf

RFC 1076: HEMS Monitoring and Control Language ๐ŸŽ€

Okay bestie, here's the header info first ๐Ÿ’–

  • Network Working Group ๐ŸŒธ
  • Request for Comments: 1076
  • Authors: G. Trewitt (Stanford University) and C. Partridge (BBN/NNSC) ๐Ÿซถ
  • Date: November 1988 ๐Ÿ“…
  • Obsoletes: RFC 1023 ๐Ÿ’…

No category or "Updates" line shows up in this header, so I'm not inventing one ๐Ÿ™ˆ

Here's the table of contents, copied exactly:

TABLE OF CONTENTS

1.   Status of This Memo                                               1
     Introduction                                                      2
2.   Overview and Scope                                                2
3.   Overview of Query Processor Operation                             4
4.   Encoding of Queries and Responses                                 5
4.1  Notation Used in This Proposal                                    5
5.   Data Organization                                                 6
5.1  Example Data Tree                                                 7
5.2  Arrays                                                            8
6.   Components of a Query                                             9
7.   Reply to a Query                                                 10
8.   Query Language                                                   12
8.1  Moving Around in the Data Tree                                   14
8.2  Retrieving Data                                                  15
8.3  Data Attributes                                                  16
8.4  Examining Memory                                                 18
8.5  Control Operations:  Modifying the Data Tree                     19
8.6  Associative Data Access:  Filters                                21
8.7  Terminating a Query                                              26
9.   Extending the Set of Values                                      27
10.  Authorization                                                    27
11.  Errors                                                           28
I.   ASN.1 Descriptions of Query Language Components                  29
I.1  Operation Codes                                                  30
I.2  Error Returns                                                    31
I.3  Filters                                                          33
I.4  Attributes                                                       34
I.5  VendorSpecific                                                   36
II.  Implementation Hints                                             36
III. Obtaining a Copy of the ASN.1 Specification                      42

1. Status of This Memo โœจ

Okay so this RFC is a query language for monitoring and control of network entities. It supersedes RFC-1023, extending the query language and giving way more discussion of the underlying issues ๐Ÿงš

This language is a component of the High-Level Entity Monitoring System (HEMS) described in RFC-1021 and RFC-1022. Readers may wish to consult these RFCs when reading this memo. RFC-1024 has the detailed assignments of numbers and structures used in this system. The parts of RFC-1024 that define query language structures are superseded by definitions in this memo. This memo assumes a knowledge of the ISO data encoding standard, ASN.1 ๐Ÿค“

Distribution of this memo is unlimited ๐ŸŒˆ

Introduction ๐Ÿฆ‹

This RFC is all about designing a general-purpose, yet efficient, monitoring and control language for managing network entities ๐Ÿ’œ The data in the entity is modeled as a hierarchy, and specific items are named by giving the path from the root of the tree. Most items are read-only, but some can be "set" to perform control operations. Both requests and responses are represented using the ISO ASN.1 data encoding rules โœจ

2. Overview and Scope ๐ŸŒธ

okay so the basic model of monitoring and control here is that a query gets sent to a monitored entity, and the entity sends back a response ๐Ÿ’Œ The term query is used in the database sense, so it may request information, modify data, or both. We'll use gateway-oriented examples, but this query-response mechanism works for any IP entity ๐ŸŒ

There's no interactive "conversation" going on, unlike SMTP [RFC-821] or FTP [RFC-959] ๐Ÿ™…โ€โ™€๏ธ A query is a complete request that stands on its own and gets a complete response back.

To design the query language, we had to define a model for the data the queries retrieve, which meant making some understanding and assumptions about that data. We ended up with a pretty flexible data model that puts few limits on the type or size of the data ๐Ÿ’…

Wherever possible, we explain the reasoning behind design decisions or assumptions that led to particular features or definitions. Some of the important global considerations and assumptions are:

  • The query processor should place as little computational burden on the monitored entity as possible ๐Ÿซถ
  • A monitored entity should not need to store the complete query ๐Ÿ™ˆ Nothing in the query language should stop an implementation from processing the query on the fly, producing portions of the response while the query is still being read and parsed โœจ Other constraints may require large amounts of data to be buffered, but the query language design must not be one ๐Ÿ’€
  • It is assumed that there is some mechanism to transport a sequence of octets to a query processor within the monitored entity, and some mechanism to return a sequence of octets to the entity making the query. In HEMS, this is provided by HEMP and its underlying transport layer. The query language design is independent of these details, however, and could be grafted onto some other protocol ๐Ÿฆ‹
  • The data model must provide organization for the data, so that it can be conveniently named ๐ŸŽ€
  • Much of the data to be monitored will be contained in tables. Some tables may contain other tables. The query language should be able to deal with such tables ๐Ÿ“‹
  • We don't provide capabilities for data reduction in the query language. We will provide for data selection, for example, only retrieving certain table entries, but we will not provide general facilities for processing data, such as computing averages ๐Ÿ™…โ€โ™€๏ธ
  • Because one monitoring center may be querying many (possibly heterogeneous) hosts, it must be possible to write generic queries that can be sent to all hosts, and have the query elicit as much information as is available from each host. I.e., queries must not be aborted just because they requested non-existent data ๐ŸŒˆ

There were some assumptions that we specifically did not make:

  • It is up to the implementation to choose what degree of concurrency will be allowed when processing queries. By locking only portions of the database, it should be possible to achieve good concurrency while still preventing deadlock ๐Ÿ”ฅ
  • This specification makes no statement about the use of the "definite" and "indefinite" length forms in ASN.1. There is currently some debate about this usage in the ISO community, so implementors should note the recommendations in the ASN.1 specification ๐Ÿคทโ€โ™€๏ธ

Other RFCs associated with HEMS are:

  • RFC-1021: Overview
  • RFC-1022: Transport protocol and message encapsulation
  • RFC-1024: Precise data definitions

The rest of this report is organized as follows ๐Ÿ—บ๏ธ

  • Section 3: gives a brief overview of the data model and the operation of the query processor.
  • Section 4: describes the encoding used for queries and responses, and the notation used to represent them in this report.
  • Section 5: describes how the data is organized in the monitored entity, and the view provided of it by the query processor.
  • Section 6: describes the basic data types that may be given to the query processor as input.
  • Section 7: describes how a reply to a query is organized.
  • Section 8: describes the operations available in the query language.
  • Section 9: describes how the set of data in the tree may be extended.
  • Section 10: describes how authorization issues affect the execution of a query.
  • Section 11: describes how errors are reported, and their effect on the processing of the query.
  • Appendix I: gives precise ASN.1 definitions of the data types used by the query processor.
  • Appendix II: gives extensive implementation hints for the core of the query processor.

3. Overview of Query Processor Operation ๐Ÿงš

In this section we give an overview of how the query processor operates, to set up a framework for the later sections ๐Ÿ’–

The query language models the manageable data as a tree. Each branch represents a different aspect of the entity, such as different layers of protocols. Subtrees are further divided to add structure to the data, and the leaves of the tree hold the actual data ๐ŸŒณ

Given this data representation, the query processor's whole job is to traverse this tree and retrieve (or modify) the data a query asks for. A query is a bunch of instructions to move around in the tree and to retrieve (or modify) named data. The result of a query is an exact image of the parts of the tree that the query processor visited ๐Ÿ“ธ

The query processor is very simple. It only understands eight commands, most of which share the same structure ๐Ÿ˜ญ It helps to think of it as an automaton that walks around the tree, directed by commands in the query. As it moves, it copies the tree structure it traverses into the query result. Data that is requested by the query gets copied into the result as well. Data that is changed by a query gets copied into the result after the modification is made ๐Ÿ‘‘

4. Encoding of Queries and Responses ๐Ÿ“

Both queries and responses are encoded using the representation defined in ISO Standard ASN.1 (Abstract Syntax Notation 1). ASN.1 represents data as sequences of <tag,length,contents> triples that are encoded as a stream of octets ๐Ÿงถ The data tuples may be recursively nested to represent structured data, such as arrays or records. For a full description, see the ISO standards IS 8824 and IS 8825. See the appendix for info on how to get these documents ๐Ÿ“š

4.1 Notation Used in This Proposal โœจ

okay so the notation in this memo is basically ASN.1's chill cousin ๐Ÿ’…. It's less formal, smaller, and (hopefully!) way easier to read ๐ŸŽ€. We're calling a <tag,length,contents> tuple a "data object" ๐Ÿงš. Heads up: we're not gonna care about the object lengths here. They're totally in the real ASN.1 encoding, but we leave them out of the examples ๐Ÿ™ˆ.

Data objects with no internal ASN.1 structure, like integer or octet string, are "simple types" or "simple objects". Objects built out of other ASN.1 data objects are "composite types" or "composite objects" ๐Ÿ’œ.

A simple object gets written like this:

ID(value)

where its tag is "ID" and it holds the given value. A composite object looks like this:

ID{ ... contents ... }

where contents is a sequence of data objects. The contents can include both simple and structured types, so the whole structure is fully recursive ๐ŸŒ€.

The difference between simple and composite types is close to the meaning of the "constructor" bit in ASN.1. For the uses here, the distinction is made based upon the semantics of the data, not the representation. So even though an OctetString can be represented in ASN.1 using either constructed or non-constructed forms, it's conceptually a simple type with no internal structure, and it will always be written as ID("some arbitrary string") in this RFC ๐Ÿซถ.

Sometimes you need to specify a type but don't have a value, like when you're referring to the name of the data. In that case we use the same notation with the value left out:

ID   or  ID()   or   ID{}

These objects have zero length and no contents. The last two forms only show up when you want to make a distinction between simple and composite data, but the difference is just notation. The representation is the same ๐Ÿฆ‹.

ASN.1 has four "classes" of tags: universal, application-specific, context-dependent, and reserved. HEMS and this query language use the first three. Universal tags are assigned in the ASN.1 standard and its addendums for common types, and any application using ASN.1 understands them. Application-specific tags are limited in scope to one particular application. They're used for "well-known" identifiers that must be recognizable in any context, like derived data types. Then there are context-dependent tags, which are for objects whose meaning depends on where they show up. Most tags that identify data are context-dependent ๐Ÿ‘‘.


5. DATA ORGANIZATION ๐Ÿ—‚๏ธ

Data in a monitored entity is modeled as a hierarchy. Implementations are not required to organize the data internally as a hierarchy, but they must provide this view of the data through the query language. A hierarchy is super useful for the following operations:

Organization    A hierarchy allows related data to be grouped
                together in a natural way.

Naming          The name of a piece of data is just the path from the
                root to the data of interest.

Mapping onto ASN.1
                ASN.1 can easily represent a hierarchy by using a
                "constructor" type as an envelope for an entire
                subtree.

Efficient Representation
                Hierarchical structures are compact and can be
                traversed quickly.

Safe Locking    If it is necessary to lock part of the hierarchy (for
                example, when doing an update), locking an entire
                subtree can be done efficiently and safely, with no
                danger of deadlock.

We're going to call this whole structure the "data tree" ๐ŸŒธ. This internal model is completely independent of the external ASN.1 representation, and any other suitable representation would work too. For efficiency's sake, we do make a one-to-one mapping between ASN.1 tags and the (internal) names of the nodes. The same could be done for any other external representation โœจ.

Each node in the hierarchy must have names for its component parts. We'd normally think of names as ASCII strings like "input errors," but the actual name is just an ASN.1 tag. These names are small integers (typically, less than 30), so the monitored entity can easily map them onto its internal representation ๐Ÿ“.

We use the term "dictionary" to mean an internal node in the hierarchy. Leaf nodes contain the actual data. A dictionary can hold both leaf nodes and other dictionaries ๐Ÿ’….

5.1 Example Data Tree

Here's a possible organization of the hierarchy in an entity that has several network interfaces and does IP routing. The exact organization of data in entities is specified in RFC-1024. This skeletal data tree is going to show up all over this RFC in query examples, so get comfy ๐Ÿ›‹๏ธ.

          System {
                  name                            -- host name
                  clock-msec                      -- msec since boot
                  interfaces                      -- # of interfaces
                  memory
                  }
          Interfaces {                            -- one per interface
                  InterfaceData{ address, mtu, netMask, ARP{...}, ... }
                  InterfaceData{ address, mtu, netMask, ARP{...}, ... }
                                  :
                  }
          IPRouting {
                  Entry{ ip-addr, interface, cost, ... }
                  Entry{ ip-addr, interface, cost, ... }
                                  :
                  }

There are three top-level dictionaries in this hierarchy (System, Interfaces, and IPRouting) and three other dictionary types (InterfaceData, Entry, and ARP), and each one has multiple instances ๐ŸŽ€.

The "name" of the clock in this entity would be:

system{ clock-msec }

and the name of a routing table entry's IP address would be IPRouting{ Entry{ ip-addr } }.

More than one piece of data can be named by a single ASN.1 object. The entire collection of system information is named by system, and the name of a routing table's IP address and cost would be IPRouting{ Entry{ ip-addr, cost } } ๐Ÿฆ‹.

5.2 Arrays

There's one sub-type of a dictionary that's used as the basis for tables of objects with identical types. We call these dictionaries arrays. In the example above, the dictionaries for interfaces, routing tables, and ARP tables are all arrays ๐ŸŒˆ.

In the examples above, the "ip-addr" and "cost" fields are named. But these names refer to the field values for ALL of the routing table entries. The name doesn't (and can't) specify which routing table entry is meant, and this ambiguity is a problem wherever data is organized in tables ๐Ÿ˜ญ. If there was a meaningful index for such tables (e.g., "routing table entry #1"), there would be no problem. Unfortunately, there usually isn't one. The fix is to access the data based on some of its content, and filters (discussed in section 8.6) provide exactly that mechanism ๐Ÿ”ฅ.

The main difference between arrays and plain dictionaries is that arrays may contain only one type of item, while dictionaries, in general, will contain many different types of items. For example, the dictionary IPRouting (which is an array) will contain only items of type Entry ๐Ÿ’–.

Just because these objects look like arrays or tables from the outside doesn't mean they're stored as linear lists inside an implementation. Any collection of same-typed objects is viewed as an array, even if it's stored internally in some other format, like a hash table ๐Ÿงš.


6. COMPONENTS OF A QUERY ๐ŸŽ€

A HEMS query is a sequence of ASN.1 objects, interpreted by a simple stack-based interpreter. [Although we define the query language in terms of the operations of a stack machine, the language does not require an implementation to use a stack machine. This is a well-understood model, and it's easy to implement.] One ASN.1 tag is reserved for operation codes, and all other tags indicate data that will eventually be used by an operation. These objects get pushed onto the stack when they're received. Opcodes are executed immediately and may remove or add items to the stack. Because ASN.1 itself provides tags, very little needs to be done to the incoming ASN.1 objects to make them suitable for use by the query interpreter ๐Ÿ’….

Each ASN.1 object in a query fits into one of the following categories:

Opcode -- An opcode tells the query interpreter to perform an action. They're described in detail in section 8. Opcodes are represented by an application-specific type whose value determines the operation ๐Ÿ‘‘.

Template -- These are objects that name one or more items in the data tree. Named items may be either simple items (leaf nodes) or entire dictionaries, in which case the entire subtree "underneath" the dictionary is understood. Templates are used to select specific data to be retrieved from the data tree. A template may be either simple or structured, depending on what it's naming. A template only names the data, so there are no values contained in it. Therefore the leaf objects in a template will all have a length of zero ๐ŸŒธ.

Examples of very simple templates are:

name()   or   System{}

Each of these is just one ASN.1 data object, with zero length. The first names a single data item in the "System" dictionary (and must appear in that context), and the second names the entire "System" dictionary. A more complex template such as:

Interfaces{ InterfaceData{ address, netMask, ARP } }

names two simple data items and a dictionary, iterated over all occurrences of "InterfaceData" within the Interfaces array ๐Ÿฆ‹.

Path -- A path is a special case of a template that names only a single node in the tree. It specifies a path down into the dictionary tree and names exactly one node in the dictionary tree ๐Ÿ“.

Value -- These are used to give data values when needed in a query, for example, when changing a value in the data tree. A value can be thought of as either a filled-in template or as the ASN.1 representation some part of the data tree ๐Ÿงš.

Filter -- A boolean expression that can be executed in the context of a particular dictionary, used to select or not select items in that dictionary. The expressions consist of the primitives "equal", "greater-or-equal", "less-or-equal", and "present", possibly joined by "and", "or", and "not". (See section 8.6.) ๐Ÿ”ฅ

Values, Paths, and Templates usually have names in the context-dependent class, except for a few special cases, which are in the application-specific class ๐Ÿ’œ.

7. REPLY TO A QUERY โœจ

okay so the data that goes back to the monitoring entity is a sequence of ASN.1 data items ๐ŸŽ€ Think of the reply as a slice of the data tree, where the query decides which bits you get. That's exactly true for retrieval requests and essentially true for modification requests too, since the reply has the data after the change went through ๐Ÿ’… The big thing to remember is that the data in a reply shows the state of the data tree right after the query ran โœจ

The order of the data comes from the order of the query language operations and the order of the data items inside the Templates and Values you give as input. If a query asks for data from two of the top-level dictionaries by giving two templates like this:

          System{ name, interfaces }
          Interfaces{

                  InterfaceData { address, netMask, mtu }
                  }

then the response is two ASN.1 data objects, like so:

          System {
                  name("system name"),
                  interfaces(2)
                  }
          Interfaces {
                  InterfaceData { address(36.8.0.1),
                                  netMask(FFFF0000),
                                  mtu(1500)
                                  }
                  InterfaceData { address(10.1.0.1),
                                  mtu(1008),
                                  netMask(FF000000)
                                  }
                  }

With a few exceptions, every data item in the hierarchy is named in the context-specific ASN.1 type space. Because of that, the returned objects must be fully qualified ๐Ÿซถ For example, the name of the entity must always come back wrapped inside an ASN.1 object for "System". If it didn't, you'd have no way to tell whether you got "name" inside "System" or "address" inside "interfaces" (assuming, in this case, that "name" and "address" were given the same integer as their ASN.1 tags) ๐Ÿ’€

Fully qualified data makes decoding easier at the receiving end, and it lets tags be chosen locally. Definitions for tags within routing tables won't clash with tags within interfaces, so the people doing the name assignments are less constrained ๐Ÿงš Also, most of the identifiers will be fairly small integers, which is a win because ASN.1 can fit tag numbers up to 30 in a one-octet tag field. Larger numbers need a second octet ๐Ÿ”ฅ

If data is requested that doesn't exist, either because the tag isn't defined or because an implementation doesn't provide that data (like when the data is optional), the response contains an ASN.1 object that's empty. The tag matches the one in the query, and the object has a length of zero ๐Ÿ™ˆ

The exact same response comes back if the data does exist but the invoker isn't authorized to access it. See section 10 for more on authorization mechanisms ๐Ÿ”

This is what lets completely generic queries get composed without anyone needing to know whether the data is defined or implemented on every entity that receives the query. All of the available data comes back, and nothing throws errors that could stop the query from being processed ๐ŸŒˆ


8. QUERY LANGUAGE ๐Ÿ’…

The query language is designed to be expressive enough to write useful queries, yet simple enough to be easy to implement ๐Ÿ“ The query processor should be as simple and fast as possible so it doesn't put a burden on the monitored entity, which may be a critical node like a gateway ๐Ÿšฆ

Queries are formed in a flexible way using what we call a "language," but it is not a programming language ๐Ÿ™…โ€โ™€๏ธ There are operations that act on data, but most of the other features of programming languages aren't there. In particular:

  • Programs are not stored in the query processor.
  • The only form of temporary storage is a stack, of limited depth.
  • There are no subroutines.
  • There are no explicit control structures defined in the language.

The central element of the language is the stack ๐Ÿฅž It may contain templates (and therefore paths), values, and filters taken from the query. It can also contain dictionaries (and therefore arrays) from the data tree. At the start of a query, it holds one item: the root dictionary.

The overall operation is reading ASN.1 objects from the input stream. Every object that isn't an opcode gets pushed onto the stack as soon as it's read. Each opcode runs immediately and may remove items from the stack, may generate ASN.1 objects and send them to the output stream, and may leave items on the stack. Since each input object is dealt with right away, parts of the response can be generated while the query is still arriving ๐ŸŒธ

In the descriptions below, operator names are in capital letters. The arguments they take from the stack come before them, and the results they leave on the stack come after. For example:

   OP                             a b   OP   a t

This means the OP operator takes <a> and <b> off of the stack and leaves <t> on the stack. Most operators in the query language leave the first operand (<a> in this example) on the stack for future use.

If both <a> and <b> were received as part of the query (as opposed to being calculated by earlier operations), then this part of the query would have consisted of the sequence:

       <a>
       <b>
       OP

So, just like other stack-based languages, the arguments and operators have to be presented in postfix order, with an operator following its operands ๐Ÿง 

Here's a summary of all the operators in the query language ๐Ÿ’– Most of them can take several different sets of operands and behave differently depending on the operand types. Details and examples come later.

   BEGIN                   dict1 path   BEGIN   dict1 dict
                    array path filter   BEGIN   array dict

BEGIN moves down in the data tree, setting up a context for future operations.

   END                           dict   END   --

END undoes the most recent BEGIN.

   GET                           dict   GET   dict
                        dict template   GET   dict
                array template filter   GET   array

GET retrieves data from the data tree.

   GET-ATTRIBUTES
                                 dict   GET-ATTRIBUTES   dict
                        dict template   GET-ATTRIBUTES   dict
                array template filter   GET-ATTRIBUTES   array

GET-ATTRIBUTES retrieves attribute information about data in the data tree.

   GET-RANGE   dict path start length   GET-RANGE   dict

GET-RANGE retrieves a subrange of an OctetString. It's used for reading memory ๐Ÿง 

   SET                     dict value   SET   dict
                   array value filter   SET   array

SET changes values in the data tree, possibly performing control functions โšก

   CREATE                 array value   CREATE   dict

CREATE makes new table entries ๐Ÿ†•

   DELETE                array filter   DELETE   array

DELETE removes table entries ๐Ÿ—‘๏ธ

These operators are defined so that it is impossible to generate an invalid query response. A response is supposed to be a snapshot of a portion (or portions) of the data tree, so it's important that only data that's actually in the tree goes into the response. Two features of the language guarantee this ๐Ÿ”’

  • Data is put in the response directly from the tree (by GET-*). Data does not go from the tree to the stack and then into the response.
  • Dictionaries on the stack are all derived from the initial, root dictionary. The operations that manipulate dictionaries (BEGIN and END) also update the response with the new location in the tree.

8.1 Moving Around in the Data Tree ๐Ÿ—บ๏ธ

The initial point of reference in the data tree is the root. That means operators name data starting at the root of the tree. It's really useful to be able to move to some other dictionary in the tree and then name data from that point ๐Ÿฆ‹ BEGIN moves down in the tree, and END undoes the last unmatched BEGIN.

BEGIN is used for two purposes:

  • By moving to a dictionary closer to the data of interest, the name of the data can be shorter than if the full name (from the root) were given.
  • It's used to establish a context for filtered operations to operate in. Filters are discussed in section 8.6.
   BEGIN                   dict1 path   BEGIN    dict1 dict

BEGIN follows <path> down the dictionary starting from <dict1>. It pushes the final dictionary named by <path> onto the stack. <path> must name a dictionary (not a leaf node). At the same time, it produces the beginning octets of an ASN.1 object corresponding to the new dictionary. It's up to the implementation to choose between the "indefinite length" representation or the "definite length" form, and going back to fill in the length later ๐Ÿงฉ

   END                           dict   END   --

END pops <dict> off of the stack and terminates the open ASN.1 object(s) started by the matching BEGIN. It must be paired with a BEGIN. If an END operation pops the root dictionary off of the stack, the query is terminated ๐Ÿ›‘

<path> must point to a regular dictionary. If any part of it refers to a non-existent node, if it points to a leaf node, or if it refers to a node inside an array-type dictionary, then it is in error, and the query is terminated immediately. Yikes, so be careful with those paths ๐Ÿ˜ญ

An additional form of BEGIN, which takes a filter argument, is described later ๐Ÿ”ฎ

8.2 Retrieving Data ๐Ÿ’–

okay so every retrieval operation follows the same vibe ๐Ÿซถ You hand it a template, and it fills in the leaf nodes of that template with the real data values โœจ

   GET                  dict template   GET   dict

GET takes a dict and a template, and it emits an ASN.1 object with the same "shape" as the template, just with values filled in for each node ๐ŸŽ€ The first ASN.1 tag of <template> should refer to an object in <dict>. If a dictionary tag shows up anywhere in <template>, the entire dictionary contents get emitted to the response ๐ŸŒธ Any items in the template that aren't in <dictionary> (or its components) come back as objects with a length of zero ๐Ÿ™ˆ

                                 dict   GET   dict

If there's no template at all, GET just grabs every item in the dictionary ๐Ÿ’… It's the same as passing a template that lists all of the items in the dictionary, so no cap, it's a shortcut ๐Ÿฆ‹

There's an extra form of GET that takes a filter argument, and we'll get to that later ๐Ÿงš

Here's an example of using BEGIN to move down the data tree to the TCP dictionary, then using GET to pull 5 data values from the TCP Stats dictionary ๐Ÿ‘‘

    IPTransport{ TCP } BEGIN
    Stats{ octetsIn, octetsOut, inputPkts, outputPkts, badtag } GET
    END

And this might return:

    IPTransport{ TCP
        Stats{ octetsIn(13255), octetsOut(82323),
               inputPkts(9213), outputPkts(12425), badtag() }
    }

"badtag" is a tag value that's undefined ๐Ÿ’€ No value comes back for it, which is how you know there's no data value associated with it. Iconic honestly ๐ŸŒˆ


8.3 Data Attributes โœจ

okay this part is so important ๐Ÿ˜ญ ASN.1 "self-describes" the structure and syntax of the data, but it tells you nothing about what the data means ๐Ÿ™ˆ Like, you can look at the raw bytes and figure out that an item is of type [context 5] and is 4 octets long. Cool, but how do you interpret it? Is it an integer, an IP address, or a 4-character string? And what does it mean? IP address of what? ๐Ÿ’€ Even if the data were "tagged" in ASN.1 parlance, that would only give you the base type (like IP-address or counter), not the meaning ๐Ÿซ 

Most of the time, this info comes from RFC-1024, which defines the ASN.1 tags and their precise meaning ๐Ÿ“š When extensions have been made, though, it may not be possible to get documentation on them. (Extensions are discussed in section 9 ๐Ÿฆ‹)

The GET-ATTRIBUTES operator is kind of like GET, except it returns a set of attributes that describe the data instead of the data itself โœจ The goal is that a human can understand what the data means, and a sophisticated application can treat the data appropriately ๐Ÿ’– Such an app could use the attribute info to format the data on a display, and decide whether it's appropriate to subtract one sample from another ๐Ÿ”ฅ

Some of the attributes are textual descriptions that help a human understand the nature of the data and give it meaningful labels ๐ŸŽ€ Extensive descriptions of standard data are optional, since they're defined in RFC-1024. But complete descriptions of extensions must be available, even if they're documented in a user's manual. As the text puts it, "Network firefighters may not have a current manual handy when the network is broken." ๐Ÿš’๐Ÿ˜ญ Iykyk

The format of the attributes isn't as simple as the format of the data itself ๐ŸŒธ You can't use the data's tag, since that would look exactly like the data itself. So here's the format:

   Attributes ::= [APPLICATION 3] IMPLICIT SEQUENCE {
           tagASN1         [0] IMPLICIT INTEGER,
           valueFormat     [1] IMPLICIT INTEGER,
           longDesc        [2] IMPLICIT IA5String OPTIONAL,
           shortDesc       [3] IMPLICIT IA5String OPTIONAL,
           unitsDesc       [4] IMPLICIT IA5String OPTIONAL,
           precision       [5] IMPLICIT INTEGER OPTIONAL,
           properties      [6] IMPLICIT BITSTRING OPTIONAL,
           valueSet        [7] IMPLICIT SET OF valueDesc OPTIONAL
           }

GET-ATTRIBUTES is similar to GET ๐Ÿ’… The major difference is that dictionaries named in the template do not pull in data for the entire subtree ๐Ÿ™…โ€โ™€๏ธ

                        dict template   GET-ATTRIBUTES   dict

It emits a single ASN.1 Attributes object for each of the objects named in <template> ๐Ÿฆ‹ For each one, the tagASN1 field gets set to the corresponding tag from the template, and the rest of the fields are set as appropriate for the data object. Any items in the template that aren't in <dictionary> (or its components) get an Attributes object with a valueFormat of NULL and no other descriptive information ๐Ÿ’€

                                 dict   GET-ATTRIBUTES   dict

And if there's no template, GET-ATTRIBUTES emits Attribute objects for all of the items in the dictionary ๐ŸŒˆ That's the same as providing a template listing every item, and it lets you get a complete list of a dictionary's contents ๐Ÿ“

There's an extra form of GET-ATTRIBUTES that takes a filter argument, which is described later ๐Ÿงš

Here's an example of using GET-ATTRIBUTES to request attributes for three objects in the System dictionary ๐Ÿ‘‘

    System{ name, badtag, clock-msec } GET-ATTRIBUTES

"badtag" is some unknown tag ๐Ÿ’€ The result might look like this:

    System{
            Attributes{
                    tagASN1(name),
                    valueFormat(IA5String),
                    longDesc("The primary hostname."),
                    shortDesc("hostname")
            },
            Attributes{
                    tagASN1(badtag), valueFormat(NULL)
            }
            Attributes{
                    tagASN1(clock-msec),
                    valueFormat(Integer),
                    longDesc("milliseconds since boot"),
                    shortDesc("uptime"), unitsDesc("ms")
                    precision(4294967296),
                    properties(1)

Note that in this example, "name" and "clock-msec" are integer values for the ASN.1 tags for the two data items ๐Ÿค“ And "badtag" is an integer value that has no corresponding name in this context ๐Ÿ’œ

There will always be exactly as many Attributes items in the result as there are objects in the template โœจ Attributes objects for items that don't exist in the entity get a valueFormat of NULL, and none of the optional elements will appear ๐Ÿ™ƒ

[ Okay, real talk, a much cleaner method would be to store the attributes as sub-components of the data item of interest ๐Ÿซถ For example, requesting System{ clock-msec } GET would normally just get the value of the data. Asking for an additional layer down the tree would now get its attributes: System{ clock-msec{ shortDesc, unitsDesc } GET would get the named attributes. (The attributes would be named with application-specific tags.) Unfortunately, ASN.1 doesn't provide a notation to describe this type of organization. So, we're stuck with the GET-ATTRIBUTES operator. However, if a cleaner organization were possible, this decision would have been made differently. ] ๐Ÿ˜ค


8.4 Examining Memory ๐Ÿง 

Even with the ability to symbolically access all of this information in an entity, there will still be times when you need to get down to very low levels and examine memory, like in remote debugging operations ๐Ÿ”ง The building blocks we've already covered can easily be extended to allow memory to be examined, so no panic ๐Ÿซถ

Memory is modeled as an ordinary object in the data tree, with an ASN.1 representation of OctetString ๐Ÿ’– Because there are so many addressing architectures out there, the conversion from the internal memory model to OctetString is very machine-dependent ๐Ÿ˜ตโ€๐Ÿ’ซ The only simple case is for byte-addressed machines with 8 bits per byte ๐Ÿ“

Each address space in an entity is represented by one "memory" data item ๐ŸŒธ In a one-address-space situation, this dictionary will probably be in the "System" dictionary ๐Ÿ‘‘ If each process has its own address space, then one "memory" item might exist for each process. Again, this is very machine-dependent ๐Ÿ™ˆ

Although the GET-RANGE operator is provided primarily for retrieving the contents of memory, it can be used on any object whose base type is OctetString โœจ

   GET-RANGE   dict path start length   GET-RANGE   dict

GET-RANGE gets <length> elements of the OctetString, starting at <start>. Both <start> and <length> are ASN.1 INTEGER type ๐Ÿ”ข The <path>, starting from <dict>, must specify a node representing memory, or some other OctetString ๐Ÿซ 

The returned data may not be <length> octets long, since it may take more than one octet to represent one memory location ๐Ÿงฉ

Memory items in the data tree are special, though ๐Ÿฅบ They won't automatically be returned when the entire contents of a dictionary are requested. e.g., if memory is part of the "System" dictionary, then this query:

   System GET

will emit the entire contents of the System dictionary, but not the memory item ๐Ÿ™…โ€โ™€๏ธ๐Ÿ’…

8.5 Control Operations: Modifying the Data Tree โœจ

okay so every operator we've covered so far only lets you retrieve data from an entity ๐Ÿ‘€ literally read-only vibes. But if you swap the template argument in the GET operators for an actual value, you can change data in the entity ๐Ÿ’…. Very few items in the data tree can be changed though, and RFC-1024 is where you find out which ones (they're noted there) ๐ŸŒธ.

Changing values is how you tweak configuration parameters, patch routing tables, and stuff like that ๐ŸŽ€. Control functions, though, like bringing an interface "down" or "up", don't usually map straight onto changing a value ๐Ÿ™ˆ. So when that's the case, an item in the tree can be defined to have arbitrary side-effects when a value gets assigned to it โœจ. Then your control operation is just "setting" that item to an appropriate command code ๐Ÿงš. Reading the item back might give you the current status ๐Ÿ“Š, and the details for those data tree items are again in RFC-1024 ๐Ÿฆ‹.

This "virtual command-and-status register" model is seriously powerful ๐Ÿ‘‘. An implementation can extend it to provide whatever controls it needs ๐Ÿ”ฅ. The best part is that the control function lives right next to the controlled object in the data tree ๐Ÿซถ. No extra language features are needed, and the same operations you use to locate data for retrieval also describe what's being controlled ๐Ÿ’œ.

For all of the control and data modification operations, the fill-in-the-blank model from data retrieval gets extended too ๐Ÿงฉ. The response is the affected part of the data tree, after the operation has been executed โœจ. So for normal execution, SET and CREATE hand back the object you gave as an argument, and DELETE returns nothing (because the thing it touched is gone, bestie ๐Ÿ’€).

SET                     dict value   SET   dict
          Set the value(s) of data in the entity to the value(s)
          given in <value>.  The result will be the value of the data
          after the SET.  Attempting to set a non-settable item will
          not produce an error, but will yield a result in the reply
          different from what was sent.

CREATE                 array value   CREATE   dict
          Insert a new entry into <array>.  Depending upon the
          context, there may be severe restrictions about what
          constitutes a valid <value>.  The result will be the actual
          item added to the <array>.  Note that only one item can be
          added per CREATE operation.

DELETE                array filter   DELETE   array
          Delete the entry(s) in <array> that match <filter>.
          Filters are described later in this document.  Normally, no
          data items will be produced in the response, but if any of
          the items that matched the filter could not be deleted,
          they will be returned in the response.

An additional form of SET, which takes a filter argument, is described later on ๐Ÿ”ฎ.

Here's an example of trying to use SET to change the number of interfaces in an entity ๐Ÿ™ƒ:

System{ interfaces(5) } SET

Since that isn't a settable parameter, the result would be:

System{ interfaces(2) }

giving you the old value. Bold move, but the data tree is not having it ๐Ÿ’….

Here's how CREATE would be used to add a routing table entry for net for 128.89.0.0 ๐Ÿ—บ๏ธ:

IPRouting BEGIN                   -- get dictionary
Entries{ DestAddr(128.89.0.0), ... }    -- entry to insert
CREATE
END                                 -- finished with dict

The result would be what was added:

IPRouting{ Entries{ DestAddr(128.89.0.0), ... } }

The results in the response of these operators are consistent with the global model of the response ๐ŸŽฏ: they contain a subset of the data in the tree, as it looks immediately after the query is executed.

Note that CREATE and DELETE only operate on arrays, and only on arrays that are specifically intended for it ๐Ÿ“‹. For example, it's totally reasonable to add and remove entries from routing tables or ARP tables, since both are arrays ๐Ÿฆ‹. However, adding or removing entries in the "Interfaces" dictionary makes no sense, because the contents of that array are dictated by the hardware ๐Ÿ”ง. For each array in the data tree, RFC-1024 tells you whether CREATE and DELETE are valid ๐Ÿ“–.

CREATE and DELETE are always invalid in non-array contexts ๐Ÿšซ. If DELETE were allowed on monitored data, the deleted data would become unmonitorable to the entire world ๐Ÿ˜ฑ. Conversely, if it were possible to CREATE arbitrary dictionary entries, there would be no way to give those entries any meaning ๐ŸŒ€. Even with the data in place, nothing would couple the data to the operation of the monitored entity ๐Ÿ’”. [Creation and deletion would also add considerable complication to an implementation, because without them, all of the data structures that represent the data tree are essentially static, with the exception of dynamic tables such as the ones mentioned, which already have mechanisms in place for adding and removing entries.]

8.6 Associative Data Access: Filters ๐ŸŽ€

okay so there's a big question we haven't answered yet: when you're dealing with array data, how do you pick out one or more entries based on some value inside them? ๐Ÿค” Picture a bunch of interfaces. The data might look like this:

Interfaces {                            -- one per interface
        InterfaceData { address, mtu, netMask, ARP{...}, ... }
        InterfaceData { address, mtu, netMask, ARP{...}, ... }
                        :
        }

If you only care about one interface (maybe because there's a ton of data on each one ๐Ÿ˜ฉ), you need some way to name it. You could just number them and say InterfaceData(3) for the third one.

But that doesn't really work, because interface numbers can change over time, maybe from one reboot to the next. It's even messier with big arrays, like routing tables or TCP connections. Big, changing arrays are honestly the more common case. Because indexing isn't very useful here, the language doesn't offer any general indexing mechanism. ๐Ÿ’…

The better move is to select objects based on a value they contain, like an IP address. That's where the query language's filters come in. A filter picks out the specific table entries an operator will act on. The operators BEGIN, GET, GET-ATTRIBUTES, SET, and DELETE can all take a filter argument, which limits them to the entries that match.

A filter is a boolean expression that gets run against every element in an array. If an entry "matches" the filter (meaning the filter comes out "true"), the operation uses it. Filter expressions are very restricted. They can only compare data stored in the array element, and they can only compare against constants. You can connect comparisons with AND, OR, and NOT. ๐Ÿงš

The ASN.1 definition of a filter is:

Filter          ::= [APPLICATION 2] CHOICE {
                        present         [0] DataPath,
                        equal           [1] DataValue,
                        greaterOrEqual  [2] DataValue,
                        lessOrEqual     [3] DataValue,
                        and             [4] SEQUENCE OF Filter,
                        or              [5] SEQUENCE OF Filter,
                        not             [6] Filter
                        }

DataPath        ::= ANY                 -- Path with no value

DataValue       ::= ANY                 -- Single data value

This is similar to the filters in the ISO monitoring protocol (CMIP), and it was derived from that spec. Very iconic lineage ๐Ÿ‘‘

"DataPath" is the name of a single data item, and "DataValue" is the value of a single data item. The three comparisons all have the same shape, "data OP constant". data is the value from the tree, constant is the value from the filter expression, and OP is one of equal, greater-than-or-equal, or less-than-or-equal. The last one, present, just checks whether the named item exists in the data tree. Since there's nothing to compare, you only give it a path and no value. ๐Ÿฆ‹

Here's an example of a filter that matches an Interface whose IP address is 10.1.0.1:

Filter{ equal{ address(10.0.0.51) } }

Notice that the name of the data being compared is relative to the InterfaceData dictionary. ๐ŸŒธ

Each operator that takes a filter argument needs an array (a dictionary that only holds one type of item) as its first argument. In our example, that's Interfaces. Every item in it is type InterfaceData, and that tag is called the "iteration tag". โœจ

Before you run a filtered operation, you have to BEGIN so the array (dictionary) is sitting on top of the stack. That sets it up as the context the filter loops over. A filtered operation then works like this:

  1. Iterate over the items in the array.
  2. For each element in the array, run the filter.
  3. If the filter succeeds, do the requested operation (GET, SET, etc.) on the matched element, using the template, value, or path as input. At this point, the operation runs exactly like it would without a filter.

This is a model of how it behaves, not a rule for how real implementations have to work. They can use whatever lookup tricks suit the table (array) in question. ๐Ÿง 

So a filtered operation has three inputs:

  1. The "current dictionary" on the stack. This is the array-type dictionary to search, set by an earlier BEGIN.
  2. A filter, which tests each item in the array. Every path or value in the filter has to be named in the context of an item in the array, as if that item were the current dictionary. For example, if a filtered operation loops over the InterfaceData items in the Interfaces array, each value or path in the filter should name something in the InterfaceData dictionary, like address.
  3. A template, path, or value for the operation. The leading ASN.1 tag in it has to match the iteration tag. In our example, where the filter searches the Interfaces dictionary, the first tag in the template/tag/value has to be InterfaceData. ๐Ÿ’–

The operators that take filters are:

BEGIN            array path filter   BEGIN   array dict

Find a dictionary in array that matches filter. Use that as the starting point for path, then push the dictionary that path points to onto the stack. If more than one dictionary matches filter, any of the matches can be used. This spec doesn't say how the choice gets made. At least one dictionary has to match, and it's an error if none do. (Maybe it should be an error to have multiple matches too, but we'd need real-world experience to decide. ๐Ÿคทโ€โ™€๏ธ)

GET          array template filter   GET   array

For each item in array that matches filter, fill in the template with values from the data tree and emit the result. The first tag of template has to equal the iteration tag. Selected parts of the matched items get emitted based on template, just like in a non-filtered GET.

GET-ATTRIBUTES
             array template filter   GET-ATTRIBUTES   array

Same as GET, except it emits attributes instead of data values. ๐ŸŽ€

SET             array value filter   SET   array

Same as GET, except it sets the values in value instead of retrieving them. Several values in the data tree get changed if the filter matches more than one item in the array. ๐Ÿ”ฅ

DELETE                array filter   DELETE   array

Deletes the entry or entries in array that match filter. ๐Ÿ—‘๏ธ

Some notes about how filters run:

  • Expressions run through an inorder tree traversal.
  • The filter operations are all GETs and comparisons, so running a filter has no side effects. That means an implementation can run only as much of the filter as it needs to get a result. For example, it can skip the rest of an AND if the first comparison comes out false.
  • It's not an error for a filter to test a data item that isn't in the data tree. The comparison just fails (it's false). So filters don't need to check whether optional data exists before comparing it. ๐Ÿ™Œ

Here's how filtering gets you the input and output packet counts for the interface with IP address 10.0.0.51:

Interfaces BEGIN                      -- dictionary
InterfaceData{ pktsIn, pktsOut }      -- template
Filter{ equal{ address(10.0.0.51) } }
GET
END                                   -- finished with dict

The returned value would be something like:

Interfaces{                             -- BEGIN
  InterfaceData{ pktsIn(1345134), pktsOut(1023729) }
                                        -- GET
}                                       -- END

The comments show which operator in the query produces each part of the response. ๐Ÿงฉ

Here's an example of a table nested inside another table. Say we want the ARP table for the interface with IP address 36.8.0.1, and we want the whole ARP entry for the host with IP address 36.8.0.23. To grab a single entry from the ARP table with a filtered GET, you first need a BEGIN to get down into the ARP table. Since the ARP table lives inside the Interfaces dictionary (an array), you need a filtered BEGIN for that step.

Interfaces BEGIN                        -- dictionary
InterfaceData{ ARP }                    -- path
Filter{ equal{ address(36.8.0.1) } }    -- filter
BEGIN                                   -- filtered BEGIN
-- Now in ARP table for 38.0.0.1; get entry for 38.8.0.23.
addrMap                                 -- whole entry
Filter{ equal{ ipAddr(36.8.0.23) } }    -- filter
GET                                     -- filtered GET
END
END

The result would be:

Interfaces{                             -- first BEGIN
  InterfaceData{ ARP{                   -- second BEGIN
    addrMap{ ipAddr(36.8.0.23), physAddr(..) } -- from GET
  } }                                   -- first END
}                                       -- second END

Check out which parts of the output come from which parts of the query. ๐Ÿ‘€

Here's how SET would shut down the interface with IP address 10.0.0.51 by changing its status to "down":

Interfaces BEGIN                    -- get dictionary
Interface{ Status(down) }           -- value to set
Filter{ equal{ IP-addr(10.0.0.51) } }
SET
END

If the SET works, the result would be:

Interfaces{                             -- BEGIN
    Interface{ Status(down) }           -- from SET
}                                       -- END

8.7 Terminating a Query ๐ŸŒˆ

A query ends by itself once there are no more ASN.1 objects left for the interpreter to process. In a perfectly formed query, the interpreter ends up right back where it started. The stack has only the root dictionary on it, and every ASN.1 object in the result is closed. โœจ

If there are still "open" ASN.1 objects in the result (because the query leaves off some ENDs), they get closed anyway, as if the right number of ENDs had been supplied. You can spot this situation when there are dictionaries other than the root dictionary sitting on the stack. ๐Ÿซถ

If an extra END comes in that would pop the root dictionary off the stack, the query ends right away. No error gets generated. ๐Ÿ’…

9. EXTENDING THE SET OF VALUES โœจ

okay so there are two ways to add new values to the query language ๐Ÿงš

the first way is to register your data and what it means, then get an ASN.1 tag assigned for it. this is the preferred method bc it makes that data spec available for literally everyone to use ๐Ÿ’–

the second way is to use the VendorSpecific application type to "wrap" your vendor-specific data ๐ŸŽ€ wherever an implementation defines data that is not in RFC-1024, the "VendorSpecific" tag should be used to label a dictionary holding that vendor data ๐Ÿ’… for example, say a vendor had some interface data that was too strange to get standard numbers for. they could represent it like this:

interfaces {
        interface {
                in-pkts, out-pkts, ...
                VendorSpecific { ephemeris, declination }
                }
        }

in this case, ephemeris and declination are two context-dependent tags the vendor assigned for their non-standard data ๐Ÿฆ‹

if the vendor-specific method is chosen, the private data MUST have descriptions available through the GET-ATTRIBUTES operator ๐Ÿšจ even with that descriptive ability, the preferred method is still to get standard numbers assigned if possible ๐ŸŒธ


10. AUTHORIZATION ๐Ÿ”

this spec doesn't say what type of authorization system is used, if any ๐Ÿ™ƒ different systems might need different mechanisms (authorization levels, capability sets, etc.), and some systems might not care about authorization at all. the only effect an authorization system has on a query is restricting which data items in the tree may be retrieved or modified.

so there are no explicit query language features that deal with protection ๐Ÿ›ก๏ธ instead, protection is implicit and may make some of the data invisible (for GET) or non-writable (for SET):

  • each query runs with some level of authorization or set of capabilities, decided by its environment (HEMS and the HEMP header)
  • each data item in the data tree has some sort of test to figure out whether a query's authorization should grant it access to that item

authorization tests only apply to the operations that retrieve info (GET, GET-ATTRIBUTES, and GET-RANGE) or modify it (SET, CREATE, DELETE). an authorization system must not affect the operation of BEGIN and END. in particular, the authorization must not hide entire dictionaries, because that would make a BEGIN on such a dictionary fail, terminating the entire query ๐Ÿ’€


11. ERRORS ๐Ÿšจ

if some particular info is requested but isn't available, it comes back as "no-value" by giving the ASN.1 length as 0 ๐Ÿ™ˆ

when there's any other kind of error, like having improper arguments on top of the stack or trying to BEGIN when the path doesn't refer to a dictionary, an ERROR object gets emitted. the contents of this object identify the exact nature of the error:

Error ::= [APPLICATION 0] IMPLICIT SEQUENCE {
        errorCode       INTEGER,
        errorInstance   INTEGER,
        errorOffset     INTEGER
        errorDescription IA5String,
        errorOp         INTEGER,
        }

errorCode identifies what the error was, and errorInstance is an implementation-dependent code that gives a more precise indication of where the error occurred. errorOffset is the location within the query where the error occurred. if an operation was being executed, errorOp contains its operation code, otherwise zero. errorDescription is a text string that can be printed and gives some description of the error. it will at least describe the errorCode, but may also give details implied by errorInstance. detailed definitions of all the fields are in appendix I.2 ๐Ÿ“š

since there may be several unterminated ASN.1 objects in progress when the error happens, each one must be terminated ๐Ÿงฉ each unterminated object gets closed with a copy of the ERROR object. depending on the type of length encoding used for this object, that means either filling in the value for the length (definite length form) or emitting two zero octets (indefinite length form). after all objects are terminated, a final copy of the ERROR object is emitted. this structure guarantees the error will be noticed at every level of interpretation on the receiving end ๐Ÿซถ

in summary, if there was an error before any ASN.1 objects were generated, the result would simply be:

error{...}

if a couple of ASN.1 objects were unterminated when the error occurred, the result might look like:

interfaces{
     interface { name(...) type(...) error{...} }
     error{...}
     }
error{...}

it would be possible to define a "WARNING" object with a similar (or same) format as ERROR, used to annotate responses when a non-fatal "error" occurs, like attempting to SET/CREATE/DELETE and the operation is denied. that would be an additional complication, so we left it out in the interests of simplicity ๐Ÿ“


I. ASN.1 DESCRIPTIONS OF QUERY LANGUAGE COMPONENTS ๐Ÿ“

a query consists of a sequence of ASN.1 objects, like this:

Query := IMPLICIT SEQUENCE of QueryElement;

QueryElement ::= CHOICE {
        Operation,
        Filter,
        Template,
        Path,
        InputValue
        }

Operation and Filter are defined below. the others are:

Template        ::= any
Path            ::= any
InputValue      ::= any

these three are all similar, but have different restrictions on their structure:

  • Template specifies a portion of the tree, naming one or more values, but not containing any values.
  • Path specifies a single path from one point in the tree to another, naming exactly one value, but not containing a value.
  • InputValue gives a value to be used by a query language operator.

a query response consists of a sequence of ASN.1 objects, as follows:

Response := IMPLICIT SEQUENCE of ResponseElement;

ResponseElement ::= CHOICE {
        ResultValue,
        Error
        }

Error is defined above. the others are:

ResultValue     ::= any

ResultValue is similar to Template, above:

  • ResultValue specifies a portion of the tree, naming and containing one or more values.

the distinctions between these are elaborated in section 6 ๐Ÿง 

I.1 Operation Codes ๐ŸŽฏ

operation codes are all encoded in a single application-specific type, whose value determines the operation to be performed. the definition is:

Operation ::= [APPLICATION 1] IMPLICIT INTEGER {
        reserved(0),
        begin(1),
        end(2),
        get(3),
        get-attributes(4),
        get-range(5),
        set(6),
        create(7),
        delete(8)
        }

I.2 Error Returns ๐Ÿ’…

okay so when something goes wrong while a query is being processed, the reply comes back with an Error object ๐Ÿšจ bestie this is giving 'it's the reply's way of saying "not today." The definition is kind of a cousin of the HEMP protocol error structure ๐Ÿงš, and the error codes were picked so the two code spaces stay totally separate. That makes error messages way easier to sort through. See section 11 for more info ๐ŸŒธ

Error ::= [APPLICATION 0] IMPLICIT SEQUENCE { errorCode INTEGER, errorInstance INTEGER, errorOffset INTEGER errorDescription IA5String, errorOp INTEGER, }

Here's what each field does ๐Ÿ’–

  • errorCode tells you the general cause of the error.
  • errorInstance is an implementation-dependent code that points more precisely to where the error happened inside the query processor. It's most useful when internal errors get reported.
  • errorOffset is the spot in the query where the error was detected. The first octet of the query is numbered zero ๐ŸŽ€
  • errorOp holds the operation code if an operation was running when things went sideways, and it's zero otherwise.
  • errorDescription is a printable text string that describes the error. It will at least describe the errorCode, and it may also spill the details hinted at by errorInstance ๐Ÿซถ

Some errors come from specific operations, and others come from the overall way the query interpreter works. So the error codes are split into two groups โœจ

The first group is about overall interpreter operation. Except for "unknown operation," these do not set errorOp ๐Ÿ™ˆ

  • 100 Other error. Any error not listed below. ๐Ÿ“
  • 101 Format error. An error was detected in the format of the input stream, which stops the query from being interpreted any further.
  • 102 System error. The query processor failed in some way because of an internal error ๐Ÿ’€
  • 103 Stack overflow. Too many items got pushed onto the stack.
  • 104 Unknown operation. The operation code is invalid. errorOp is set here.

The second group is for errors tied to running particular operations. errorOp will always be set for these ๐Ÿ‘‘

  • 200 Other operation error. Any error tied to an operation that isn't listed below ๐ŸŒˆ
  • 201 Stack underflow. An operation expected to see some number of operands on the stack, and there were fewer items on the stack than that.
  • 202 Operand error. An operation expected certain operand types on the stack, and something else was sitting there instead ๐Ÿ˜ญ
  • 203 Invalid path for BEGIN. The path given for BEGIN was invalid, because some element in the path didn't exist.
  • 204 Non-dictionary for BEGIN. The path given for BEGIN was invalid, because the node it pointed to was a leaf node, not a dictionary.
  • 205 BEGIN on array element. The path pointed at an array element. The path has to point at a single, unique node, so a filtered BEGIN should have been used instead โญ
  • 206 Empty filter for BEGIN. The filter for a BEGIN didn't match any array element.
  • 207 Filtered operation on non-array. A filtered operation was tried on a regular dictionary. Filters can only be used on arrays, periodt ๐Ÿ’…
  • 208 Index out of bounds. The starting address or length for a GET-RANGE operation went outside the bounds of the given object.
  • 209 Bad object for GET-RANGE. GET-RANGE can only be applied to objects whose base type is OctetString ๐Ÿฆ‹

This list is probably not quite complete. It will need to be extended based on what implementations actually run into ๐Ÿงš


I.3 Filters ๐ŸŽ€

Lots of the operations can take a filter argument to pick out elements in an array. We already talked about them in section 8.6 ๐Ÿ’–

Filter ::= [APPLICATION 2] CHOICE { present [0] DataPath, equal [1] DataValue, greaterOrEqual [2] DataValue, lessOrEqual [3] DataValue, and [4] SEQUENCE OF Filter, or [5] SEQUENCE OF Filter, not [6] Filter }

DataPath ::= ANY -- Path with no value

DataValue ::= ANY -- Single data value

A filter gets executed by an inorder traversal of its ASN.1 structure ๐ŸŒธ The basic filter operations are:

  • present checks whether a particular data item exists in the data tree ๐Ÿ”ฅ
  • equal checks whether the named data item is equal to the given value.
  • greaterOrEqual checks whether the named data item is greater than or equal to the given value.
  • lessOrEqual checks whether the named data item is less than or equal to the given value.

You can mix these with "and", "or", and "not" operators to build pretty much any boolean expression you want ๐Ÿง  The "and" and "or" operators take any number of terms. Terms only get evaluated up to the point where the outcome is decided. So an "and" term that comes out false, or an "or" term that comes out true, stops the evaluation right there โœจ


I.4 Attributes ๐ŸŒˆ

One or more Attributes structures get returned by the GET-ATTRIBUTES operator. They describe items in the data tree, and section 8.3 has the full tea โ˜•

Attributes ::= [APPLICATION 3] IMPLICIT SEQUENCE { tagASN1 [0] IMPLICIT INTEGER, valueFormat [1] IMPLICIT INTEGER, longDesc [2] IMPLICIT IA5String OPTIONAL, shortDesc [3] IMPLICIT IA5String OPTIONAL, unitsDesc [4] IMPLICIT IA5String OPTIONAL, precision [5] IMPLICIT INTEGER OPTIONAL, properties [6] IMPLICIT BITSTRING OPTIONAL, valueSet [7] IMPLICIT SET OF valueDesc OPTIONAL } valueDesc ::= IMPLICIT SEQUENCE { value [0] ANY, -- Single data value desc [1] IA5String }

Here's what each attribute means ๐Ÿ’…

  • tagASN1 is the ASN.1 tag for this object. This attribute is required ๐Ÿ“
  • valueFormat is the underlying ASN.1 type of the object, like SEQUENCE or OCTETSTRING or Counter. It isn't just the tag number, though. It's the entire tag, the way it would show up in an ASN.1 object, so it includes the class, which should be either UNIVERSAL or APPLICATION. Applications receiving this should ignore the constructor bit. This attribute is required ๐Ÿ”ฅ
  • longDesc is a potentially lengthy text description that fully defines the object. It's optional for objects defined in this memo and required for entity-specific objects ๐Ÿ“š
  • shortDesc is a short mnemonic string of less than 15 characters, good for labeling the value on a display. This attribute is optional โœจ
  • unitsDesc is a short string used for integer values to show the units the value is measured in, like "ms", "sec", "pkts", etc. This attribute is optional ๐Ÿฆ‹
  • precision is, for Counter objects, the value where the Counter rolls over. It's required for all Counter objects ๐ŸŒ™
  • properties is a bitstring of boolean properties for the object. If a bit is on, the object has that property. This attribute is optional. The bits defined right now are:
    • Bit 0: If true, the difference between two values of this object is significant. For example, changes in a packet count always matter, because they always convey information, so the 0 bit would be set. On the other hand, the difference between two readings of a queue length may be meaningless ๐Ÿงš
    • Bit 1: If true, the value may be modified with SET, CREATE, and DELETE. Whether CREATE and DELETE apply depends on whether the object is in an array ๐ŸŽ€
    • Bit 2: If true, the object is a dictionary, and a BEGIN may be used on it. If false, the object is a leaf node in the data tree ๐ŸŒธ
    • Bit 3: If true, the object is an array-type dictionary, and filters may be used to traverse it. (Bit 2 will be true also.) ๐Ÿ‘‘
  • valueSet is for data that's defined as an ASN.1 CHOICE type, which is an enumerated type. It gives descriptions for each of the possible values the data object can take on. Each valueDesc is a <value,description> pair. This is super important for control items, which are very likely to show up in VendorSpecific dictionaries. That's exactly the situation where you really need descriptive info ๐Ÿฅน

I.5 VendorSpecific ๐Ÿงš

See the discussion in section 9 ๐Ÿฆ‹

VendorSpecific ::= [APPLICATION 4] IMPLICIT SET of ANY

II. IMPLEMENTATION HINTS โœจ

okay bestie, RFCs normally don't do implementation stuff ๐Ÿ™ˆ but the authors were like "we have to give some tips," so here they are ๐Ÿ’… This list isn't complete. It's just hints about how they imagine the query processor could be built efficiently ๐Ÿ”ฅ

  • Okay so the stack is super limited in depth ๐Ÿ“š Because of how the query language works, it only gets about 4 entries (for arguments) plus the depth of the tree (up to one BEGIN per level in the tree). That comes out to about a dozen entries in the stack, which is a modest ask ๐ŸŽ€
  • The stack is an abstraction, so use pointers, not copying dictionaries around, ngl that would be so wasteful ๐Ÿ’€
  • An object-oriented approach makes this way easier โœจ Changes to the "shape" of the data items (which will certainly happen, early on) are also easier to make ๐Ÿฆ‹
  • Only a few "messages" need to be understood by objects ๐Ÿงš If each node in the tree has pointers to action routines for each basic operation (GET,SET,...), common routines (like emitting a long integer located at address X) can be shared, and special routines (like setting the interface state for interface X) can live in a common framework. Higher levels need to know nothing about what data is being dealt with ๐Ÿ‘‘
  • Most interesting objects are dictionaries ๐Ÿ’œ Each one can be implemented using pointers to the data, plus procedure "hooks" that perform specific operations like GET, SET, filtering, etc.
  • The hardest part is actually pulling the data out of existing TCP/IP implementations that weren't designed with detailed monitoring in mind ๐Ÿ˜ญ Query processors talking to a UNIX kernel have to make lots of system calls to get the more intricate structures, like routing tables. This is way less painful if a system is designed with easy monitoring as a goal ๐ŸŒธ

A Skeletal Implementation ๐Ÿฆ‹

This section gives a pretty detailed example of the core of a query processor ๐Ÿ“ The code has not been tested, and it's only here to give implementors ideas about tackling some parts of query processor implementation with finesse instead of brute force ๐Ÿ’…

The suggested architecture is for each dictionary to have a "traverse" routine attached to it, which gets called whenever any sort of operation has to be done on that dictionary. Most nodes share the same traversal routine, but array dictionaries usually have routines that know about whatever special lookup mechanisms they need ๐Ÿงš

Non-dictionary nodes would have two routines, "action" and "compare", which implement query language operations and filter comparisons, respectively. Most nodes would share these routines ๐Ÿซถ

For example, there should be one "action" routine that does query language operations on 32-bit integers, and another that works on 16-bit integers, etc. ๐Ÿ”ฅ

Any traversal procedure would take arguments like:

       traverse(node, mask, op, filter)
               Treenode        node;   /* generic node-in-tree */
               ASN             mask;   /* internal ASN.1 form */
               enum opset      op;     /* what to do */
               Filter          filter; /* zero if no filter */

       enum opset { begin, get, set, create, delete, geta,
                       c_le, c_ge, c_eq, c_exist };

The traversal procedure is called whenever anything has to be done within a dictionary. The arguments are:

  • node is the current dictionary.
  • mask is either the template, path, or value, depending on the operation being performed. The top-level identifier of this object will be looked up in the context of <node>.
  • op is the operation to be performed, either one of the basic operations, or a filter operation.
  • filter is the filter to be applied, or zero if there isn't one. There will be no filter when <op> is a filter-comparison operation.

The big idea is that the traversal proc attached to a node has all of the knowledge about how to get around in that subtree encoded inside it ๐ŸŒˆ Hopefully this will be the only place that knowledge is coded. Here is a skeleton of the "standard" traversal proc, written mostly in C ๐Ÿ’–

When the query processor needs to execute a "GET" operation, it would just call:

       traverse(current, template, GET, 0)

Notes about this example:

  • This traversal routine handles either query language operations (GET, SET, etc.) or low-level filter operations. Separate routines could be defined for the two classes of operations, but they do a lot of the same work ๐Ÿ™ˆ
  • Dictionary nodes have a <traversal> proc defined ๐ŸŽ€
  • Leaf nodes have an <action> proc, which implements GET, SET, GET-ATTRIBUTES, CREATE, and DELETE, and a <compare> proc, which performs low-level filter comparisons ๐Ÿ’œ
  • In the generic routine, the filter argument is unused, because the generic routine isn't used for array dictionaries, and only array dictionaries use filters ๐Ÿฆ‹
  • An ASN type contains the top level tag and a list of sub-components ๐Ÿงš
  • size(mask) takes an ASN.1 object and tells you how many sub-items are in it. Zero means it's a simple object โœจ
  • lookup(node, tag) looks up a tag in the given (tree)node and returns a pointer to the node. If the tag doesn't exist in that node, a pointer to a special node "NullItem" is returned. NullItem looks like a leaf node and has procs that perform the correct action for non-existent data ๐ŸŒธ
  • This example does not do proper error handling, or ASN.1 generation, both of which would need additional code in this routine ๐Ÿ˜ญ

Here's the standard traversal proc itself ๐Ÿ‘‘

       /*
        *  For op = GET/SET/etc, return:
        *              true on error, otherwise false.
        *  When op is a filter operation, return:
        *              the result of the comparison.
        */
       int std_traverse(node, mask, op, filter)
           Treenode    node;   /* current node */
           ASN         mask;   /* internal ASN.1 form */
           enum opset  op;     /* what to do */
           Filter      filter; /* unused in this routine */
       {
           ASN         item;
           Treenode    target;
           boolean     rv = false;
           extern Treenode NullItem;

           if (filter != null) {
               error(...);
               return true;
           }

           target = lookup(node, mask.tag);

           /*  We are at the leaf of the template/path/value.  */
           if (size(mask) == 0)
               switch (op)
               {
               case BEGIN:
                   /*  non-existent node, or leaf node  */
                   if (target == NullItem || target.traverse == 0) {
                       error(...);
                       return true;
                       }
                   else {
                       begin(node, mask.tag);
                       return false;
                       }

               case GET:       case SET:       case GETA:

               case GETR:      case CREATE:    case DELETE:
                   /*  A leaf in the mask specifies entire directory.
                       For GET, traverse the entire subtree.  */
                   if (target.traverse)
                       if (op == GET) {
                           foreach subnode in target
                               /*  Need to test to not GET memory.  */
                               rv |= (*target.traverse)
                                       (target, subnode.tag, op, 0);
                           return rv;
                       }
                       else if (op == SET)     /*  no-op  */
                           return false;
                       else if (op != GETA) {
                           error(...);
                           return true;
                       }
                   /*  We're at a leaf in both the mask and the tree.
                       Just execute the operation.
                   */
                   else {
                       if (op == BEGIN) {  /*  Can't begin on leaf  */
                           error(...);
                           return true;
                       else
                           return (*target.action)(target, mask, op);
                       }
                   }  /* else */

               default:        /*  Comparison ops.  */
                   return (*target.compare)(target, mask, op);
               }  /* switch */

           /*  We only get here if mask has structure.  */

           /*  can't have multiple targets for BEGIN  */
           if (op == BEGIN && size(mask) != 1) {
               error(...);
               return true;
           }
           /*  or for a single filter operation.  */
           if (op is comparison && size(mask) != 1) {
               error(...);
               return false;
           }
           /*  Iterate over the components in mask  */
           foreach item in mask
           {

               if (target.traverse)    /*  traverse subtree.  */
                   rv |= (*component.traverse)(component, item, op, 0);
               else                    /*  leaf node, at last.  */
                   if (op is comparison)
                       return (*target.compare)(target, mask, op);
                   else
                       return (*target.action)(target, mask, op);
           } /* foreach */

           return rv;
       }  /* std_traverse */

Here's a bare skeleton of an array-type dictionary's traversal proc ๐ŸŒˆ

       int array_traverse(node, mask, op, filter)
           Treenode    node;   /* current node */
           ASN         mask;   /* internal ASN.1 form */
           enum opset  op;     /* what to do */
           Filter      filter; /* unused in this routine */
       {
           Treenode    target;
           boolean     rv = false;
           extern Treenode NullItem;

           /*  Didn't find that key.  */
           if (mask.tag != this array's iteration tag)
               return false;

           if (op == BEGIN && filter == null) {
               error(...);
               return 1;
           }

           /*  The implementation of this loop is the major trick!  */
           /*  Needs to stop after first filter success on BEGIN.  */
           foreach target in node {
               if (filter == null ||           /*  if no filter, or */
                   ExecFilter(target, filter)) /* if it succeeds  */
                   rv |= (target.traverse*)(target, mask, op, 0);
           }
       }  /* array_traverse */

Object-oriented programming languages, such as C++, Modula, and Ada, are well suited to this style of implementation. There should be no particular difficulty with using a conventional language such as C or Pascal, however ๐Ÿซถ

III. OBTAINING A COPY OF THE ASN.1 SPECIFICATION ๐Ÿงš

Copies of ISO Standard ASN.1 (Abstract Syntax Notation 1) are available from the following source. It comes in two parts, and both are needed ๐Ÿ”ฅ:

       IS 8824 -- Specification (meaning, notation)
       IS 8825 -- Encoding Rules (representation)

They are available from:

       Omnicom Inc.
       115 Park St, S.E.          (new address as of March, 1987)
       Vienna, VA  22180
       (703) 281-1135
๐Ÿ“ฆ where this page comes from