RFC 1076
HEMS monitoring and control language
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: OverviewRFC-1022: Transport protocol and message encapsulationRFC-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:
- Iterate over the items in the array.
- For each element in the array, run the filter.
- 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:
- The "current dictionary" on the stack. This is the array-type dictionary to search, set by an earlier BEGIN.
- 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
InterfaceDataitems in theInterfacesarray, each value or path in the filter should name something in theInterfaceDatadictionary, likeaddress. - 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
Interfacesdictionary, the first tag in the template/tag/value has to beInterfaceData. ๐
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:
Templatespecifies a portion of the tree, naming one or more values, but not containing any values.Pathspecifies a single path from one point in the tree to another, naming exactly one value, but not containing a value.InputValuegives 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:
ResultValuespecifies 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 ๐
errorCodetells you the general cause of the error.errorInstanceis 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.errorOffsetis the spot in the query where the error was detected. The first octet of the query is numbered zero ๐errorOpholds the operation code if an operation was running when things went sideways, and it's zero otherwise.errorDescriptionis a printable text string that describes the error. It will at least describe theerrorCode, and it may also spill the details hinted at byerrorInstance๐ซถ
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.
errorOpis 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:
presentchecks whether a particular data item exists in the data tree ๐ฅequalchecks whether the named data item is equal to the given value.greaterOrEqualchecks whether the named data item is greater than or equal to the given value.lessOrEqualchecks 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 ๐
tagASN1is the ASN.1 tag for this object. This attribute is required ๐valueFormatis 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 ๐ฅlongDescis 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 ๐shortDescis a short mnemonic string of less than 15 characters, good for labeling the value on a display. This attribute is optional โจunitsDescis 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 ๐ฆprecisionis, for Counter objects, the value where the Counter rolls over. It's required for all Counter objects ๐propertiesis 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.) ๐
valueSetis 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:
nodeis the current dictionary.maskis 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>.opis the operation to be performed, either one of the basic operations, or a filter operation.filteris 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
- ietf: rfc1076 1988-11
- ๐ girlified by claude-haiku-5-5 on : a language model's rewrite of the original, under its terms