├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── functions ├── functions.go └── functions_test.go ├── oc_translate.go ├── octree ├── graph.go ├── octree.go └── octree_test.go ├── oparse ├── parser.go └── parser_test.go ├── orismologer ├── orismologer.go └── orismologer_test.go ├── proto ├── mappings.pb ├── mappings.proto ├── paths.proto ├── transformations.pb └── vendor_oids.pb ├── testdata ├── oc_tree_test_mappings.pb └── orismologer_test_transformations.pb └── utils ├── utils.go └── utils_test.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orismologer 2 | 3 | _NB: This is not an officially supported Google product_ 4 | 5 | Orismologer aims to enable clients to get OpenConfig-formatted telemetry from network devices which do not natively support it. This can support scaling of network monitoring infrastructure. 6 | 7 | Classically, telemetry is exported via SNMP, CLI scraping or, occasionally, vendor-specific API calls. These methods do not standardise the telemetry format, greatly increasing the complexity and cost of maintaining network monitoring pipelines. The [OpenConfig](openconfig.net) project is addressing this by developing vendor-agnostic data models for network telemetry. However, not all devices support OpenConfig, hence this project. 8 | 9 | Orismologer includes: 10 | 11 | 1) A protobuf-based framework for expressing translations from 'classic' telemetry to OpenConfig-formatted telemetry (see the `.proto` files in `/proto`). 12 | 2) Specific translations for Cisco and Aruba devices defined using the framework from 1) (see the `.pb` files in `/proto`). 13 | 3) Code for evaluating translations defined in the framework from 1) (see the various Go files in this project). 14 | 4) A command line interface for 3), supporting commands to a) retrieve data for an OpenConfig path from a hardware target which does not natively support OpenConfig and b) output supported OpenConfig paths (see `oc_translate.go`). 15 | 16 | ## Install 17 | 18 | - Get the code: `go get github.com/google/orismologer` 19 | - Install the [protobuf compiler](https://github.com/protocolbuffers/protobuf). Eg: 20 | - Download a [prebuilt binary](https://github.com/protocolbuffers/protobuf/releases) 21 | - Unzip it 22 | - Move it onto your `$PATH`: `sudo cp ~/Downloads/bin/protoc /usr/local/bin` 23 | - Install the [Go protobuf tools](https://github.com/golang/protobuf): `go get -u github.com/golang/protobuf/protoc-gen-go` 24 | - Make sure `protoc-gen-go` is on your `$PATH` (eg: `export PATH=$PATH:~/go/bin`) 25 | - Compile the Orismologer protobuf definitions (the last command needs to be re-run whenever the proto definitions are modified): 26 | 27 | ``` 28 | cd orismologer 29 | mkdir proto_out 30 | protoc --go_out=proto_out proto/*.proto 31 | ``` 32 | 33 | ## Run 34 | Get data for a supported OpenConfig path for a given hardware target. Vendor information is used to determine which OIDs can be supported. Note that in its current state the project does not retrieve any classic telemetry (but could easily be modified to do so), and the `target` flag is thus not used. See "system design" below for more information. 35 | 36 | `go run oc_translate.go get -path /system/state/boot-time -target t -vendor cisco` 37 | 38 | Output logs to stderr (NB: the flag must appear before the command). 39 | 40 | `go run oc_translate.go -alsologtostderr get -path /system/state/boot-time -target t -vendor cisco` 41 | 42 | Print all paths for which at least one translation has been defined. 43 | 44 | `go run oc_translate.go print` 45 | 46 | Print all paths in an OpenConfig subtree rooted at the given node for which at least one translation has been defined. 47 | 48 | `go run oc_translate.go print -root /system` 49 | 50 | ## Defining New Mappings 51 | New OpenConfig nodes can be added to Orismologer in `proto/mappings.pb` and new transformations can be defined in `proto/transformations.pb`. See below for an overview of these concepts. 52 | 53 | ## Test 54 | Run the project's tests like you would for any other Go project, eg: 55 | 56 | ``` 57 | cd orismologer 58 | go test ./... 59 | ``` 60 | 61 | ## System Overview 62 | 63 | Orismologer's telemetry translation framework is implemented as a protobuf schema. This section provides a brief overview of the framework and the code which uses it. Authoritative documentation can be found in comments in the relevant files in this project. 64 | 65 | ### Transformations 66 | 67 | Orismologer allows users to define logic to map from non-OpenConfig paths or "NocPaths" (like SNMP OIDs) to OpenConfig paths. This logic is split into reusable, atomic units called "transformations" (see the example below). Every transformation has an identifier (see the `bind` field) and at least one string expression which contains the translation logic (see below for more information on the expression syntax). The example below defines a transformation with the identifier "example" whose output is always `1000`: 68 | 69 | ``` 70 | transformations { 71 | bind: "memory_KB" 72 | expressions: "1000" 73 | } 74 | ``` 75 | 76 | One transformation can reuse another via its identifier. This example reuses the `memory_KB` transformation: 77 | 78 | ``` 79 | transformations { 80 | bind: "memory_MB" 81 | expressions: "memory_KB / 1000" 82 | } 83 | ``` 84 | 85 | Expressions defined in the same transformation are considered equivalent. This is especially useful for normalising equivalent NocPaths across vendors: 86 | 87 | ``` 88 | transformations { 89 | bind: "memory_MB" 90 | expressions: "memory_KB / 1000" 91 | expressions: "memory_B / 1000000" 92 | } 93 | ``` 94 | 95 | Transformations which reference other transformations can only take us so far. Ultimately concrete data has to be retrieved from a NocPath. The example transformation below defines one NocPath for Cisco and one for Aruba, normalises the output to MB, and binds the result to the `memory_MB` identifier. Other transformations can reuse this output without having to concern themselves with vendor-specific differences. Note that `memory_aruba` defines multiple OIDs. As with expressions, multiple OIDs defined in the same NocPath message are considered to produce equivalent output. 96 | 97 | _NB: At this time only SNMP OIDs are supported as NocPaths (but the process for supporting other kinds is straight-forward)._ 98 | _NB: The output of all NocPaths is assumed to be of type string. Thus expressions should call `to_X()` on NocPath output, if appropriate._ 99 | 100 | ``` 101 | transformations { 102 | bind: "memory_MB" 103 | # Both of the following expressions produce equivalent output. 104 | expressions: "to_int(memory_cisco) / 1000" # Imagine Cisco's OID outputs in KB. 105 | expressions: "to_int(memory_aruba) / 1000000" # Imagine Aruba's OID outputs in B. 106 | 107 | noc_paths { 108 | bind: "memory_cisco" 109 | oids: "1.3.6.1.4.1.9.9.168.99.99.99" # Not a real OID. 110 | } 111 | 112 | noc_paths { 113 | bind: "memory_aruba" 114 | oids: "1.3.6.1.4.1.14823.99.99.99.99" # Not a real OID. 115 | oids: "1.1.1.1.1.1.1" # Imagine that Aruba memory is available from multiple OIDs. 116 | } 117 | } 118 | ``` 119 | 120 | Thus, Orismologer's transformations form a graph where the nodes represent sets of logically equivalent statements, and the edges represent dependencies amongst them. 121 | 122 | ### Mappings 123 | 124 | NocPaths (discussed above) are at one edge of Orismologer's transformation graph. At the other is the OpenConfig tree (modeled in this project as nested proto messages). Each node declares a subpath. 125 | 126 | ``` 127 | nodes { 128 | subpath {path: "/orismologer"} 129 | } 130 | ``` 131 | 132 | A child node's OpenConfig path is formed by prefixing it with its parents subpaths. In the example below the full OpenConfig path of the child node would be `/orismologer/example/memory`. 133 | 134 | _NB: The subpath of the top-level ancestor node should start from the OpenConfig root (ie: `/orismologer` not `orismologer`)._ 135 | 136 | ``` 137 | nodes { 138 | subpath {path: "/orismologer"} 139 | children { 140 | subpath {path: "example/memory"} # Full path: /orismologer/example/memory 141 | } 142 | } 143 | ``` 144 | 145 | OpenConfig nodes link to Orismologer's transformation graph by referencing a transformation's identifier (see the `bind` field in the example below). Thus, in the example below, requesting telemetry for the (fake) OpenConfig path `/orismologer/example/memory` would yield the output of the transformation `memory_MB`. 146 | 147 | _NB: It generally only makes sense for OpenConfig leaf nodes to link to a transformation_ 148 | 149 | ``` 150 | nodes { 151 | subpath {path: "/orismologer"} 152 | children { 153 | subpath {path: "example/memory"} 154 | bind: "memory_MB" # Links to a transformation with the identifier "memory_MB". The output of this transformation will be returned for this OpenConfig path. 155 | } 156 | } 157 | ``` 158 | 159 | 160 | ### Transformation Evaluation 161 | 162 | Given an OpenConfig path and a hardware target, Orismologer can retrieve telemetry data by walking its transformation graph: 163 | 164 | - Look up the node indicated by the given path in the OpenConfig tree. 165 | - Find the transformation for that node (ie: its `bind` field). 166 | - Evaluate that transformation: 167 | - Evaluate each of its expressions in turn, proceeding with the first expression that can be evaluated and skipping the rest: 168 | - Evaluate each of the variables in the expression, rejecting the entire expression if one variable cannot be evaluated. 169 | - If a variable links to another transformation, evaluate that transformation (by repeating this process recursively). 170 | - If a variable links to a NocPath, ensure that it can be evaluated for the given hardware target. If it can, retrieve the requested data and proceed with the next variable in the expression. 171 | 172 | #### Example 173 | 174 | Imagine we have the following mapping and transformation: 175 | 176 | ``` 177 | nodes { 178 | subpath {path: "/orismologer"} 179 | children { 180 | subpath {path: "example/memory"} 181 | bind: "memory_MB" 182 | } 183 | } 184 | 185 | transformations { 186 | bind: "memory_MB" 187 | # Both of the following expressions produce equivalent output. 188 | expressions: "to_int(memory_cisco) / 1000" 189 | expressions: "to_int(memory_aruba) / 1000000" 190 | 191 | noc_paths { 192 | bind: "memory_cisco" 193 | oids: "1.3.6.1.4.1.9.9.168.99.99.99" 194 | samples: "1000" 195 | } 196 | 197 | noc_paths { 198 | bind: "memory_aruba" 199 | oids: "1.3.6.1.4.1.14823.99.99.99.99" 200 | oids: "1.1.1.1.1.1.1" 201 | samples: "1000000" 202 | } 203 | } 204 | ``` 205 | 206 | If we request output for `/orismologer/example/memory` we will see the following output, which can be traced to understand the operation of the program: 207 | 208 | ``` 209 | > go run oc_translate.go -alsologtostderr get -target t -path /orismologer/example/memory -vendor aruba 210 | I0212 16:25:04.536684 50360 orismologer.go:123] found transformation "memory_MB" for path "/orismologer/example/memory" 211 | I0212 16:25:04.536856 50360 orismologer.go:139] evaluating transformation "memory_MB" for target "t" of vendor "aruba" 212 | I0212 16:25:04.536864 50360 orismologer.go:179] storing NocPath "memory_cisco" of transformation "memory_MB" 213 | I0212 16:25:04.536871 50360 orismologer.go:179] storing NocPath "memory_aruba" of transformation "memory_MB" 214 | I0212 16:25:04.536878 50360 orismologer.go:143] evaluating expression `to_int(memory_cisco) / 1000` 215 | I0212 16:25:04.537062 50360 orismologer.go:210] evaluating variable "memory_cisco" 216 | I0212 16:25:04.537073 50360 orismologer.go:152] ignoring NocPath "memory_cisco" as it cannot be resolved for vendor "aruba" 217 | I0212 16:25:04.537080 50360 orismologer.go:156] could not evaluate all variables for expression `to_int(memory_cisco) / 1000`, continuing to next expression 218 | I0212 16:25:04.537086 50360 orismologer.go:143] evaluating expression `to_int(memory_aruba) / 1000000` 219 | I0212 16:25:04.537218 50360 orismologer.go:210] evaluating variable "memory_aruba" 220 | I0212 16:25:04.537224 50360 orismologer.go:283] Requesting NocPath "memory_aruba" from target "t" 221 | I0212 16:25:04.537229 50360 orismologer.go:229] evaluated variable "memory_aruba" = 1000000 222 | I0212 16:25:04.537238 50360 functions.go:158] Calling "to_int" with args: 1000000 223 | I0212 16:25:04.537253 50360 parser.go:442] Evaluated expression: to_int(memory_aruba) / 1e+06 = 1 224 | 1 225 | ``` 226 | 227 | 228 | ### Expression Syntax 229 | Expressions are defined in transformation proto messages. They are evaluated at runtime to carry out the operations needed to translate telemetry from one format to another. The expression syntax, by design, very simple. This limits the complexity of the expressions users can write, improving readability and maintainability, and reducing the scope for security exploits. The expression syntax supports the following features: 230 | 231 | - Integer literals. 232 | - Float literals. 233 | - String literals. 234 | - Basic arithmetic operators (+, -, *, /, ^). 235 | - Brackets, and a conventional order of operations, eg: `(3 + 7) / 2 = 5` 236 | - String concatenation, eg: `"hello" + "world" = "hello world"` 237 | - Variables. 238 | - Function calls, eg: `my_func(1, "a")` 239 | - Nested expressions (ie: expressions inside expressions), eg: `1 + my_func(2*2, other_func())` 240 | 241 | #### Calling Functions 242 | When function calls are encountered in expressions, Orismologer passes the function name (as a string) and any parameters to a function which is responsible for calling an implementation corresponding to that function name. The current implementation only supports calling predefined "library" functions, to reduce scope for security exploits. These are implemented and registered in `functions/functions.go`. 243 | 244 | 245 | ## Project Roadmap 246 | 247 | - Implement a NocPathResolver which retrieves real data (eg: from a TSDB), instead of relying on hard coded samples. 248 | - Provide a better interface for consumers of OpenConfig telemetry. 249 | - Support OpenConfig list nodes with multiple keys, eg: `node[k1, k2]` 250 | - Support nested OpenConfig list nodes (and, equivalently, nested SNMP tables). 251 | - Add support for CLI-scraped NocPaths. 252 | - Proto validation. 253 | - Support dry runs (for determining if a mapping exists for a given OpenConfig path and hardware target). 254 | - Add a vendor flag to the print subcommand. Only show paths supported for that vendor. 255 | -------------------------------------------------------------------------------- /functions/functions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package functions maps a collection of functions to string keys and facilitates calling them with 19 | these keys. 20 | Registered functions must return 1 or 2 values. If 2, then the second must be an error (or nil). 21 | */ 22 | package functions 23 | 24 | import ( 25 | "fmt" 26 | "reflect" 27 | "strconv" 28 | "strings" 29 | "time" 30 | 31 | "github.com/golang/glog" 32 | "github.com/google/orismologer/utils" 33 | ) 34 | 35 | // Functions must be registered here to expose them to external callers. 36 | var registry = map[string]interface{}{ 37 | "to_int": toInt, 38 | "to_str": toStr, 39 | "time_since_epoch": timeSinceEpoch, 40 | } 41 | 42 | // Implementations of functions. 43 | 44 | func toStr(value interface{}) (string, error) { 45 | result, ok := value.(string) 46 | if !ok { 47 | return "", fmt.Errorf("value `%v` could not be cast to string", value) 48 | } 49 | return result, nil 50 | } 51 | 52 | func toInt(value interface{}) (int, error) { 53 | if str, err := toStr(value); err == nil { 54 | if result, err := strconv.Atoi(str); err == nil { 55 | return result, nil 56 | } 57 | } 58 | result, ok := value.(int) 59 | if !ok { 60 | return 0, fmt.Errorf("value `%v` could not be cast to int", value) 61 | } 62 | return result, nil 63 | } 64 | 65 | func toFloat(value interface{}) (float64, error) { 66 | if str, err := toStr(value); err == nil { 67 | if result, err := strconv.ParseFloat(str, 64); err == nil { 68 | return result, nil 69 | } 70 | } 71 | result, ok := value.(float64) 72 | if !ok { 73 | return 0, fmt.Errorf("value `%v` could not be cast to float64", value) 74 | } 75 | return result, nil 76 | } 77 | 78 | /* 79 | timeSinceEpoch returns the amount of time since the Unix epoch (1970-01-01) in the requested units. 80 | Format can be "rfc3339", "ntp" or any time format string understood by Go's time.Parse(). 81 | Units can be "s", "ms" or "ns". 82 | */ 83 | func timeSinceEpoch(value interface{}, format string, units string) (int, error) { 84 | timeStamp, ok := value.(string) 85 | if !ok { 86 | return 0, fmt.Errorf("requested %v to unix conversion, but %q is not %v formatted", format, value, format) 87 | } 88 | var t time.Time 89 | switch format { 90 | case "ntp": 91 | timeStamp = strings.Replace(timeStamp, " ", "", -1) 92 | const offset = 2208988800 // NTP epoch is 1900-01-01. 93 | ntp, err := strconv.ParseUint(timeStamp, 16, 64) 94 | if err != nil { 95 | fmt.Println(err) 96 | } 97 | seconds := ntp>>32 - offset 98 | fractional := ntp & 0x00000000ffffffff 99 | nanos := fractional * 1000000000 >> 32 100 | t = time.Unix(int64(seconds), int64(nanos)) 101 | case "rfc3339": 102 | format = time.RFC3339 103 | fallthrough 104 | default: 105 | var err error 106 | t, err = time.Parse(format, timeStamp) 107 | if err != nil { 108 | return 0, fmt.Errorf("error parsing timestamp %q of format %q: %v", value, format, err) 109 | } 110 | } 111 | switch units { 112 | case "s": 113 | return int(t.Unix()), nil 114 | case "ms": 115 | return int(t.UnixNano() / 1000000), nil 116 | case "ns": 117 | return int(t.UnixNano()), nil 118 | default: 119 | return 0, fmt.Errorf("unrecognised unit %q", units) 120 | } 121 | } 122 | 123 | // Code to handle and call library functions. 124 | 125 | /* 126 | Library contains a predefined collection of functions which may be called via a string key. 127 | */ 128 | type Library struct { 129 | functions map[string]interface{} 130 | } 131 | 132 | // NewLibrary returns a new function library. 133 | func NewLibrary() Library { 134 | return newLibrary(registry) 135 | } 136 | 137 | func newLibrary(registry map[string]interface{}) Library { 138 | return Library{functions: registry} 139 | } 140 | 141 | /* 142 | Call calls a function from a predefined collected, given only the function's name as a string and 143 | any arguments to be passed to it. 144 | */ 145 | func (l Library) Call(funcName string, args ...interface{}) (interface{}, error) { 146 | f, err := l.getFunc(funcName) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | numArgsExpected := f.Type().NumIn() 152 | numArgs := len(args) 153 | if numArgs != numArgsExpected { 154 | return nil, fmt.Errorf("function %q expects %v arguments, but got %v", funcName, numArgsExpected, numArgs) 155 | } 156 | 157 | wrappedArgs := wrapArgs(args...) 158 | glog.Info(fmt.Sprintf("Calling %q with args: %v\n", funcName, utils.SliceToString(args))) 159 | output := f.Call(wrappedArgs) 160 | return unwrapOutput(output, funcName) 161 | } 162 | 163 | func (l Library) getFunc(funcName string) (reflect.Value, error) { 164 | if !l.Contains(funcName) { 165 | return reflect.Value{}, fmt.Errorf("function %q undefined", funcName) 166 | } 167 | return reflect.ValueOf(l.functions[funcName]), nil 168 | } 169 | 170 | // wrapArgs wraps each arg in a reflect.Value. 171 | func wrapArgs(args ...interface{}) []reflect.Value { 172 | wrappedArgs := make([]reflect.Value, len(args)) 173 | for i, arg := range args { 174 | wrappedArgs[i] = reflect.ValueOf(arg) 175 | } 176 | return wrappedArgs 177 | } 178 | 179 | // unwrapOutput unwraps output wrapped in reflect.Value. 180 | func unwrapOutput(output []reflect.Value, funcName string) (interface{}, error) { 181 | results := make([]interface{}, len(output)) 182 | for i, value := range output { 183 | results[i] = value.Interface() 184 | } 185 | switch len(results) { 186 | case 1: 187 | return results[0], nil 188 | case 2: 189 | result, wrappedErr := results[0], results[1] 190 | if wrappedErr == nil { 191 | return result, nil 192 | } 193 | err, ok := wrappedErr.(error) 194 | if !ok { 195 | return nil, fmt.Errorf("function %q returned two values, but the second was not an error. The value was: %v", funcName, wrappedErr) 196 | } 197 | return result, err 198 | default: 199 | return nil, fmt.Errorf("function %q returned unexpected number of results", funcName) 200 | } 201 | } 202 | 203 | // Contains returns true if a function with the given name has been defined. 204 | func (l Library) Contains(funcName string) bool { 205 | return l.functions[funcName] != nil 206 | } 207 | -------------------------------------------------------------------------------- /functions/functions_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package functions 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestLibraryCall(t *testing.T) { 26 | l := makeDummyLibrary() 27 | tests := []struct { 28 | name string 29 | funcName string 30 | args []interface{} 31 | expected interface{} 32 | expectsError bool 33 | }{ 34 | { 35 | name: "valid call", 36 | funcName: "dummy", 37 | args: []interface{}{"test"}, 38 | expected: "test", 39 | }, 40 | { 41 | name: "undefined function", 42 | funcName: "undefined", 43 | expectsError: true, 44 | }, 45 | { 46 | name: "too few args", 47 | funcName: "dummy", 48 | expectsError: true, 49 | }, 50 | { 51 | name: "too many args", 52 | funcName: "dummy", 53 | expectsError: true, 54 | }, 55 | { 56 | name: "one output", 57 | funcName: "oneOutput", 58 | expected: "1", 59 | }, 60 | { 61 | name: "too many outputs", 62 | funcName: "threeOutputs", 63 | expectsError: true, 64 | }, 65 | { 66 | name: "not enough outputs", 67 | funcName: "noOutputs", 68 | expectsError: true, 69 | }, 70 | { 71 | name: "second output not error or nil", 72 | funcName: "secondOutputNotError", 73 | expectsError: true, 74 | }, 75 | } 76 | for _, test := range tests { 77 | t.Run(test.name, func(t *testing.T) { 78 | got, err := l.Call(test.funcName, test.args...) 79 | argStrings := []string{fmt.Sprintf("%q", test.funcName)} 80 | for _, arg := range test.args { 81 | argStrings = append(argStrings, fmt.Sprint(arg)) 82 | } 83 | argString := strings.Join(argStrings, ", ") 84 | switch { 85 | case err != nil && !test.expectsError: 86 | t.Errorf("call(%v) expected %v, got error: %v", argString, test.expected, err) 87 | case err == nil && test.expectsError: 88 | t.Errorf("call(%v) got: %v, expected error", argString, got) 89 | case err == nil && got != test.expected: 90 | t.Errorf("call(%v) = %v, expected: %v", argString, got, test.expected) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | func TestLibraryToInt(t *testing.T) { 97 | tests := []struct { 98 | name string 99 | input interface{} 100 | expected int 101 | expectsError bool 102 | }{ 103 | { 104 | name: "valid call", 105 | input: 10, 106 | expected: 10, 107 | }, 108 | { 109 | name: "string", 110 | input: "10", 111 | expected: 10, 112 | }, 113 | { 114 | name: "float", 115 | input: 10.0, 116 | expectsError: true, 117 | }, 118 | { 119 | name: "float string", 120 | input: "10.0", 121 | expectsError: true, 122 | }, 123 | { 124 | name: "overflow", 125 | input: "999999999999999999999999999", 126 | expectsError: true, 127 | }, 128 | { 129 | name: "negative", 130 | input: -1, 131 | expected: -1, 132 | }, 133 | { 134 | name: "slice", 135 | input: []int{10}, 136 | expectsError: true, 137 | }, 138 | } 139 | for _, test := range tests { 140 | t.Run(test.name, func(t *testing.T) { 141 | got, err := toInt(test.input) 142 | switch { 143 | case err != nil && !test.expectsError: 144 | t.Errorf("toInt(%v) expected %v, got error: %v", test.input, test.expected, err) 145 | case err == nil && test.expectsError: 146 | t.Errorf("toInt(%v) got: %v, expected error", test.input, got) 147 | case err == nil && got != test.expected: 148 | t.Errorf("toInt(%v) = %v, expected: %v", test.input, got, test.expected) 149 | } 150 | }) 151 | } 152 | } 153 | 154 | func TestLibraryToStr(t *testing.T) { 155 | tests := []struct { 156 | name string 157 | input interface{} 158 | expected string 159 | expectsError bool 160 | }{ 161 | { 162 | name: "plain string", 163 | input: "thing", 164 | expected: "thing", 165 | }, 166 | { 167 | name: "integer", 168 | input: 10, 169 | expectsError: true, 170 | }, 171 | { 172 | name: "float", 173 | input: 10.0, 174 | expectsError: true, 175 | }, 176 | { 177 | name: "integer string", 178 | input: "10", 179 | expected: "10", 180 | }, 181 | { 182 | name: "float string", 183 | input: "10.0", 184 | expected: "10.0", 185 | }, 186 | { 187 | name: "slice", 188 | input: []string{"string"}, 189 | expectsError: true, 190 | }, 191 | } 192 | for _, test := range tests { 193 | t.Run(test.name, func(t *testing.T) { 194 | got, err := toStr(test.input) 195 | switch { 196 | case err != nil && !test.expectsError: 197 | t.Errorf("toInt(%v) expected %v, got error: %v", test.input, test.expected, err) 198 | case err == nil && test.expectsError: 199 | t.Errorf("toInt(%v) got: %v, expected error", test.input, got) 200 | case err == nil && got != test.expected: 201 | t.Errorf("toInt(%v) = %v, expected: %v", test.input, got, test.expected) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | func TestLibraryToFloat(t *testing.T) { 208 | tests := []struct { 209 | name string 210 | input interface{} 211 | expected float64 212 | expectsError bool 213 | }{ 214 | { 215 | name: "float", 216 | input: 10.0, 217 | expected: 10.0, 218 | }, 219 | { 220 | name: "float string", 221 | input: "10.0", 222 | expected: 10.0, 223 | }, 224 | { 225 | name: "integer", 226 | input: 10, 227 | expectsError: true, 228 | }, 229 | { 230 | name: "integer string", 231 | input: "10", 232 | expected: 10.0, 233 | }, 234 | { 235 | name: "negative", 236 | input: -1.0, 237 | expected: -1.0, 238 | }, 239 | { 240 | name: "slice", 241 | input: []int{-10.0}, 242 | expectsError: true, 243 | }, 244 | } 245 | for _, test := range tests { 246 | t.Run(test.name, func(t *testing.T) { 247 | got, err := toFloat(test.input) 248 | switch { 249 | case err != nil && !test.expectsError: 250 | t.Errorf("toFloat(%v) expected %v, got error: %v", test.input, test.expected, err) 251 | case err == nil && test.expectsError: 252 | t.Errorf("toFloat(%v) got: %v, expected error", test.input, got) 253 | case err == nil && got != test.expected: 254 | t.Errorf("toFloat(%v) = %v, expected: %v", test.input, got, test.expected) 255 | } 256 | }) 257 | } 258 | } 259 | 260 | func TestLibraryTimeSinceEpoch(t *testing.T) { 261 | tests := []struct { 262 | name string 263 | timeStamp interface{} 264 | format string 265 | units string 266 | expected int 267 | expectsError bool 268 | }{ 269 | { 270 | name: "ntp with spaces to s", 271 | timeStamp: "dfc4 0b68 8147 af78", 272 | format: "ntp", 273 | units: "s", 274 | expected: 1545178344, 275 | }, 276 | { 277 | name: "ntp without spaces to s", 278 | timeStamp: "dfc40b688147af78", 279 | format: "ntp", 280 | units: "s", 281 | expected: 1545178344, 282 | }, 283 | { 284 | name: "ntp to ms", 285 | timeStamp: "dfc40b688147af78", 286 | format: "ntp", 287 | units: "ms", 288 | expected: 1545178344505, 289 | }, 290 | { 291 | name: "ntp to ns", 292 | timeStamp: "dfc40b688147af78", 293 | format: "ntp", 294 | units: "ns", 295 | expected: 1545178344505000082, 296 | }, 297 | { 298 | name: "iso8601 to s", 299 | timeStamp: "2018-12-19 00:12:24", 300 | format: "2006-01-02 15:04:05", 301 | units: "s", 302 | expected: 1545178344, 303 | }, 304 | { 305 | name: "RFC3339 to s", 306 | timeStamp: "2018-12-19T11:12:24+11:00", 307 | format: "rfc3339", 308 | units: "s", 309 | expected: 1545178344, 310 | }, 311 | { 312 | name: "missing format", 313 | timeStamp: "2018-12-18 15:15:59", 314 | units: "s", 315 | expectsError: true, 316 | }, 317 | { 318 | name: "missing units", 319 | timeStamp: "2018-12-18 15:15:59", 320 | format: "iso8601", 321 | expectsError: true, 322 | }, 323 | } 324 | for _, test := range tests { 325 | t.Run(test.name, func(t *testing.T) { 326 | got, err := timeSinceEpoch(test.timeStamp, test.format, test.units) 327 | switch { 328 | case err != nil && !test.expectsError: 329 | t.Errorf("timeSinceEpoch(%q, %q, %q) expected %v, got error: %v", test.timeStamp, test.format, test.units, test.expected, err) 330 | case err == nil && test.expectsError: 331 | t.Errorf("timeSinceEpoch(%q, %q, %q) got: %v, expected error", test.timeStamp, test.format, test.units, got) 332 | case err == nil && got != test.expected: 333 | t.Errorf("timeSinceEpoch(%q, %q, %q) = %v, expected: %v", test.timeStamp, test.format, test.units, got, test.expected) 334 | } 335 | }) 336 | } 337 | } 338 | 339 | func makeDummyLibrary() Library { 340 | registry := map[string]interface{}{ 341 | "dummy": dummy, 342 | "noOutputs": noOutputs, 343 | "threeOutputs": threeOutputs, 344 | "oneOutput": oneOutput, 345 | "secondOutputNotError": secondOutputNotError, 346 | } 347 | return newLibrary(registry) 348 | } 349 | 350 | func dummy(arg string) string { 351 | return arg 352 | } 353 | 354 | func noOutputs() { 355 | } 356 | 357 | func oneOutput() string { 358 | return "1" 359 | } 360 | 361 | func threeOutputs() (string, string, string) { 362 | return "1", "2", "2" 363 | } 364 | 365 | func secondOutputNotError() (string, string) { 366 | return "1", "2" 367 | } 368 | -------------------------------------------------------------------------------- /oc_translate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Command oc_translate retrieves telemetry for a given OpenConfig path from a hardware target which 19 | does not natively support OpenConfig. 20 | */ 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | 26 | "flag" 27 | "github.com/google/orismologer/orismologer" 28 | ) 29 | 30 | const ( 31 | mappingsFile = "proto/mappings.pb" 32 | transformationsFile = "proto/transformations.pb" 33 | vendorOidsFile = "proto/vendor_oids.pb" 34 | ) 35 | 36 | var ( 37 | printCommand = flag.NewFlagSet("print", flag.ExitOnError) 38 | rootFlag = printCommand.String("root", "root", "print the subtree rooted "+ 39 | "at the given node") 40 | 41 | getCommand = flag.NewFlagSet("get", flag.ExitOnError) 42 | ocPathFlag = getCommand.String("path", "", "the OpenConfig path to resolve") 43 | targetFlag = getCommand.String("target", "", "the hardware target for which"+ 44 | "the OpenConfig path should be resolved") 45 | vendorFlag = getCommand.String("vendor", "", "the vendor of the hardware "+ 46 | "target") 47 | ) 48 | 49 | func printUsage() { 50 | fmt.Println(`usage: orismologer []) 51 | print Print an ASCII representation of the tree of OpenConfig nodes which Orismologer can resolve. 52 | get Resolve an OpenConfig path for a given hardware target.`) 53 | } 54 | 55 | func main() { 56 | flag.Usage = printUsage 57 | flag.Parse() 58 | 59 | o, err := orismologer.NewOrismologer(mappingsFile, transformationsFile, vendorOidsFile) 60 | if err != nil { 61 | fmt.Println(err) 62 | return 63 | } 64 | 65 | if len(flag.Args()) == 0 { 66 | fmt.Println("Provide a command") 67 | printUsage() 68 | return 69 | } 70 | 71 | switch flag.Arg(0) { 72 | case "print": 73 | printCommand.Parse(flag.Args()[1:]) 74 | case "get": 75 | getCommand.Parse(flag.Args()[1:]) 76 | default: 77 | fmt.Printf("Unknown command %q\n", flag.Arg(0)) 78 | printUsage() 79 | } 80 | 81 | if printCommand.Parsed() { 82 | o.PrintOcPaths(*rootFlag) 83 | } 84 | 85 | if getCommand.Parsed() { 86 | mandatoryArgsPresent := true 87 | if *ocPathFlag == "" { 88 | fmt.Println("supply an OpenConfig path") 89 | mandatoryArgsPresent = false 90 | } 91 | 92 | if *targetFlag == "" { 93 | fmt.Println("supply a hardware target") 94 | mandatoryArgsPresent = false 95 | } 96 | 97 | if *vendorFlag == "" { 98 | fmt.Println("supply the vendor of the hardware target") 99 | mandatoryArgsPresent = false 100 | } 101 | 102 | if mandatoryArgsPresent { 103 | result, err := o.Eval(*ocPathFlag, *targetFlag, *vendorFlag) 104 | if err != nil { 105 | fmt.Println(err) 106 | return 107 | } 108 | fmt.Println(result) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /octree/graph.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package octree 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | // Graph provides a basic public interface for graph types. It does not support multi-edges. 25 | type Graph interface { 26 | // Nodes returns all nodes in the graph. 27 | // The result should have a stable order. 28 | Nodes() []string 29 | 30 | // Neighbors returns a list of neighbors (successors) to the given source node. 31 | // The result should have a stable order. 32 | Neighbors(a string) []string 33 | 34 | // Weight returns the weight associated with the given edge. 35 | Weight(a, z string) int64 36 | } 37 | 38 | type edge struct{ a, z string } 39 | 40 | // AdjList is a mutable, directed, weighted graph implemented using an adjacency list. 41 | // It can be treated as an unweighted graph by using AddEdge() which provides default weights of 1. 42 | // It can be used as an undirected graph by using AddEdge() in each direction. 43 | type AdjList struct { 44 | nodes []string 45 | edges map[string][]string 46 | weight map[edge]int64 47 | } 48 | 49 | // NewAdjList creates a new adjacency list graph. 50 | func NewAdjList() *AdjList { 51 | return &AdjList{ 52 | nodes: []string{}, 53 | edges: make(map[string][]string), 54 | weight: make(map[edge]int64), 55 | } 56 | } 57 | 58 | // AddNode adds a node with the given name if it does not already exist. 59 | func (g *AdjList) AddNode(n string) { 60 | if _, ok := g.edges[n]; ok { 61 | return 62 | } 63 | g.nodes = append(g.nodes, n) 64 | g.edges[n] = []string{} 65 | } 66 | 67 | // AddEdge adds an edge with the given name if it does not already exist. 68 | // The default weight of 1 is used. If a graph is built only using AddEdge() it can be treated as 69 | // unweighted graph in some algorithms below. 70 | func (g *AdjList) AddEdge(a, z string) { 71 | g.AddWeightedEdge(a, z, 1) 72 | } 73 | 74 | // AddWeightedEdge adds an edge with the given name and weight if it does not already exist. 75 | func (g *AdjList) AddWeightedEdge(a, z string, weight int64) { 76 | g.AddNode(a) 77 | g.AddNode(z) 78 | g.weight[edge{a, z}] = weight 79 | for _, n := range g.edges[a] { 80 | if z == n { 81 | return 82 | } 83 | } 84 | g.edges[a] = append(g.edges[a], z) 85 | } 86 | 87 | // Nodes returns all nodes in the graph. 88 | func (g *AdjList) Nodes() []string { 89 | return g.nodes 90 | } 91 | 92 | // Edges returns all edges in the graph. 93 | func (g *AdjList) Edges() map[string][]string { 94 | return g.edges 95 | } 96 | 97 | // Neighbors returns a list of neighbors to the given source node. 98 | func (g *AdjList) Neighbors(a string) []string { 99 | return g.edges[a] 100 | } 101 | 102 | // Weight returns the edge weight for the given edge. 103 | func (g *AdjList) Weight(a, z string) int64 { 104 | return g.weight[edge{a, z}] 105 | } 106 | 107 | // ToDot renders this graph to a dot format string, which can be helpful for debugging. 108 | func (g *AdjList) ToDot() string { 109 | lines := []string{"digraph {"} 110 | for _, n := range g.Nodes() { 111 | lines = append(lines, fmt.Sprintf("\t%q", n)) 112 | } 113 | for _, n := range g.Nodes() { 114 | for _, n2 := range g.Neighbors(n) { 115 | lines = append(lines, fmt.Sprintf("\t%q -> %q [label=%d]", n, n2, g.Weight(n, n2))) 116 | } 117 | } 118 | lines = append(lines, "}") 119 | return strings.Join(lines, "\n") 120 | } 121 | -------------------------------------------------------------------------------- /octree/octree.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package octree implements a tree data structure for storing OpenConfigNode protos. 18 | package octree 19 | 20 | import ( 21 | "fmt" 22 | pb "github.com/google/orismologer/proto_out/proto" 23 | "strings" 24 | ) 25 | 26 | const ( 27 | pathSep = "/" 28 | 29 | // RootName is the name of the root node of an OcTree. 30 | RootName = "root" 31 | ) 32 | 33 | /* 34 | OcTree represents an OpenConfig tree. Nodes are represented as strings. 35 | The underlying representation is a graph (adjacency list), with a map of node payloads. 36 | */ 37 | type OcTree struct { 38 | graph *AdjList 39 | payloads map[string]*pb.OpenConfigNode 40 | } 41 | 42 | // NewTree creates and populates an OcTree from a Mappings proto. 43 | func NewTree(mappings *pb.Mappings) (OcTree, error) { 44 | t := OcTree{ 45 | graph: NewAdjList(), 46 | payloads: map[string]*pb.OpenConfigNode{}, 47 | } 48 | // Create a root OCNode so proto tree can be handled consistently. 49 | t.graph.AddNode(RootName) 50 | for _, node := range mappings.GetNodes() { 51 | if err := t.build(RootName, node); err != nil { 52 | return t, err 53 | } 54 | } 55 | return t, nil 56 | } 57 | 58 | /* 59 | build recursively creates an OcTree given an OpenConfigNode proto and a relative or absolute path to 60 | its parent. Ancestor nodes in the path will be created as needed. 61 | */ 62 | func (t *OcTree) build(parent string, current *pb.OpenConfigNode) error { 63 | subpath, err := expandPath(current.GetSubpath().GetPath()) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // Root has no parent, so if given path starts at root (absolute) advance position by one node. 69 | if len(subpath) > 0 && subpath[0] == RootName { 70 | parent = RootName 71 | subpath = subpath[1:] 72 | } 73 | 74 | // Create each node in the given path, using its absolute path as the node name. 75 | fullPath := parent 76 | for _, node := range subpath { 77 | fullPath = fullPath + pathSep + node 78 | t.addChild(parent, fullPath) 79 | parent = fullPath 80 | } 81 | 82 | // Set the leaf node's payload (only these hold interesting data; others are just structure). 83 | if err := t.setPayload(fullPath, current); err != nil { 84 | return err 85 | } 86 | 87 | // Continue to build the tree, recursively, treating the current node as the parent. 88 | children := current.GetChildren() 89 | for _, child := range children { 90 | if err := t.build(fullPath, child); err != nil { 91 | return err 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func (t *OcTree) addChild(parent string, child string) error { 98 | if !t.IsValid(parent) { 99 | return fmt.Errorf("could not add child %q as parent %q does not exist", child, parent) 100 | } 101 | t.graph.AddEdge(parent, child) 102 | return nil 103 | } 104 | 105 | // children returns the child nodes of a node in an OcTree. 106 | func (t *OcTree) children(parent string) ([]string, error) { 107 | if !t.IsValid(parent) { 108 | return nil, fmt.Errorf("could not get children of node with invalid path %q", parent) 109 | } 110 | return t.graph.Neighbors(parent), nil 111 | } 112 | 113 | func (t *OcTree) getPayload(node string) (*pb.OpenConfigNode, error) { 114 | if !t.IsValid(node) { 115 | return nil, fmt.Errorf("could not get payload as no such node in tree: %q", node) 116 | } 117 | if val, ok := t.payloads[node]; ok { 118 | return val, nil 119 | } 120 | return nil, fmt.Errorf("payload missing for node %q", node) 121 | } 122 | 123 | func (t *OcTree) setPayload(node string, payload *pb.OpenConfigNode) error { 124 | if !t.IsValid(node) { 125 | return fmt.Errorf("could not set payload as no such node in tree: %q", node) 126 | } 127 | t.payloads[node] = payload 128 | return nil 129 | } 130 | 131 | /* 132 | IsValid returns true if a given OpenConfig path is defined in the OcTree. 133 | Paths are given as "root/parent/child" or, equivalently, as "/parent/child". 134 | */ 135 | func (t *OcTree) IsValid(path string) bool { 136 | path, err := normalizePath(path) 137 | if err != nil { 138 | return false 139 | } 140 | _, ok := t.graph.Edges()[path] 141 | return ok 142 | } 143 | 144 | // GetTransformationIdentifier returns the identifier of the transformation for a given OC path. 145 | func (t *OcTree) GetTransformationIdentifier(path string) (string, error) { 146 | node, err := normalizePath(path) 147 | if err != nil { 148 | return "", err 149 | } 150 | payload, err := t.getPayload(node) 151 | if err != nil { 152 | return "", err 153 | } 154 | return payload.GetBind(), nil 155 | } 156 | 157 | // Print pretty prints a subtree rooted at the given node. 158 | func (t *OcTree) Print(root string) error { 159 | if !t.IsValid(root) { 160 | return fmt.Errorf("cannot print tree from nonexistant node %q", root) 161 | } 162 | return t._printTree(root, root, "", false) 163 | } 164 | 165 | func (t *OcTree) _printTree(originalRoot string, current string, prefix string, last bool) error { 166 | originalRoot, err := normalizePath(originalRoot) 167 | if err != nil { 168 | return fmt.Errorf("could not print tree: %v", err) 169 | } 170 | current, err = normalizePath(current) 171 | if err != nil { 172 | return fmt.Errorf("could not print tree: %v", err) 173 | } 174 | path, err := expandPath(current) 175 | if err != nil { 176 | return fmt.Errorf("could not print tree: %v", err) 177 | } 178 | nodeName := path[len(path)-1] 179 | 180 | fmt.Print(prefix) 181 | switch { 182 | case last: 183 | fmt.Print("└── ") 184 | prefix = fmt.Sprintf("%v ", prefix) 185 | case current != originalRoot: 186 | fmt.Print("├── ") 187 | prefix = fmt.Sprintf("%v| ", prefix) 188 | } 189 | fmt.Println(nodeName) 190 | 191 | children, err := t.children(current) 192 | if err != nil { 193 | return fmt.Errorf("could not print tree: %v", err) 194 | } 195 | for i, child := range children { 196 | t._printTree(originalRoot, child, prefix, i == len(children)-1) 197 | } 198 | return nil 199 | } 200 | 201 | /* 202 | expandPath takes a path string and returns it, normalized, as an array of path segments. 203 | eg: "/path/to/something" -> [root path to something] 204 | */ 205 | func expandPath(path string) ([]string, error) { 206 | path, err := normalizePath(path) 207 | if err != nil { 208 | return nil, fmt.Errorf("could not expand path: %v", err) 209 | } 210 | return strings.Split(path, pathSep), nil 211 | } 212 | 213 | func joinPath(path []string) string { 214 | return strings.Join(path, pathSep) 215 | } 216 | 217 | /* 218 | Normalize path accepts path strings and returns the canonical representation used internally in this 219 | package. Eg: 220 | 221 | /first/second -> root/first/second (expand `/` to `root/`) 222 | root/first/second -> root/first/second (no change) 223 | first/second -> first/second (relative path, so no `root` is added) 224 | 225 | It also removes trailing slashes, eg: `first/second/` becomes `first/second`. 226 | */ 227 | func normalizePath(path string) (string, error) { 228 | if path == pathSep { 229 | return RootName, nil 230 | } 231 | if strings.Contains(path, pathSep+pathSep) { 232 | return "", fmt.Errorf("invalid path %q", path) 233 | } 234 | if strings.HasSuffix(path, pathSep) { 235 | path = strings.TrimSuffix(path, pathSep) 236 | } 237 | if strings.HasPrefix(path, pathSep) { 238 | return RootName + path, nil 239 | } 240 | return path, nil 241 | } 242 | -------------------------------------------------------------------------------- /octree/octree_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package octree 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | "github.com/google/orismologer/utils" 26 | ) 27 | 28 | func TestTreeBuildsMultiSegmentSubpathsCorrectly(t *testing.T) { 29 | tree := makeTree(t) 30 | children, err := tree.children("root/grandmother/aunt") 31 | if err != nil { 32 | t.Fatalf("%v", err) 33 | } 34 | if len(children) != 1 { 35 | t.Fatalf("expect aunt to have exactly one child, but got %v", len(children)) 36 | } 37 | expected := "root/grandmother/aunt/cousin" 38 | got := children[0] // Make sure we don't get something like "root/grandmother/cousin" 39 | if got != expected { 40 | t.Fatalf("expected aunt's child to be named %q, but got %q", expected, got) 41 | } 42 | } 43 | 44 | func TestExpandPath(t *testing.T) { 45 | for _, test := range []struct { 46 | name string 47 | path string 48 | expected []string 49 | }{ 50 | { 51 | name: "just root", 52 | path: "/", 53 | expected: []string{"root"}, 54 | }, 55 | { 56 | name: "absolute path", 57 | path: "/first/second/third", 58 | expected: []string{"root", "first", "second", "third"}, 59 | }, 60 | { 61 | name: "relative path", 62 | path: "first/second", 63 | expected: []string{"first", "second"}, 64 | }, 65 | { 66 | name: "relative path, one node", 67 | path: "first", 68 | expected: []string{"first"}, 69 | }, 70 | } { 71 | t.Run(test.name, func(t *testing.T) { 72 | got, err := expandPath(test.path) 73 | if err != nil { 74 | t.Errorf("%v: expected `%v`, got error: %v", test.name, test.expected, err) 75 | } 76 | if !cmp.Equal(test.expected, got) { 77 | t.Errorf("%v: expected `%v`, got `%v`", test.name, test.expected, got) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestJoinPath(t *testing.T) { 84 | for _, test := range []struct { 85 | path []string 86 | expected string 87 | }{ 88 | { 89 | path: []string{"root"}, 90 | expected: "root", 91 | }, 92 | { 93 | path: []string{"root", "first", "second", "third"}, 94 | expected: "root/first/second/third", 95 | }, 96 | { 97 | path: []string{"first", "second"}, 98 | expected: "first/second", 99 | }, 100 | { 101 | path: []string{"first"}, 102 | expected: "first", 103 | }, 104 | } { 105 | testName := fmt.Sprintf("[%v]", strings.Join(test.path, ",")) 106 | t.Run(testName, func(t *testing.T) { 107 | got := joinPath(test.path) 108 | if !cmp.Equal(test.expected, got) { 109 | t.Errorf("%v: expected `%v`, got `%v`", test.path, test.expected, got) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | func TestChildren(t *testing.T) { 116 | tree := makeTree(t) 117 | for _, test := range []struct { 118 | name string 119 | node string 120 | expected []string 121 | }{ 122 | { 123 | name: "root's children", 124 | node: "root", 125 | expected: []string{"root/paternal_grandfather", "root/grandmother"}, 126 | }, 127 | { 128 | name: "no children", 129 | node: "root/paternal_grandfather/father/child", 130 | expected: []string{}, 131 | }, 132 | { 133 | name: "children of a nested node", 134 | node: "root/paternal_grandfather/father", 135 | expected: []string{"root/paternal_grandfather/father/child", "root/paternal_grandfather/father/sibling"}, 136 | }, 137 | } { 138 | t.Run(test.name, func(t *testing.T) { 139 | got, err := tree.children(test.node) 140 | if err != nil { 141 | t.Errorf("%v: expected `%v`, got error: %v", test.name, test.expected, err) 142 | } 143 | if !cmp.Equal(test.expected, got) { 144 | t.Errorf("%v: expected `%v`, got `%v`", test.name, test.expected, got) 145 | } 146 | }) 147 | } 148 | } 149 | 150 | func TestIsValid(t *testing.T) { 151 | tree := makeTree(t) 152 | for _, test := range []struct { 153 | name string 154 | path string 155 | expectPathIsValid bool 156 | }{ 157 | { 158 | name: "just root", 159 | path: "/", 160 | expectPathIsValid: true, 161 | }, 162 | { 163 | name: "does not contain invalid path", 164 | path: "/invalid", 165 | expectPathIsValid: false, 166 | }, 167 | { 168 | name: "contains valid path", 169 | path: "/paternal_grandfather", 170 | expectPathIsValid: true, 171 | }, 172 | { 173 | name: "contains valid nested path", 174 | path: "/paternal_grandfather/father/child", 175 | expectPathIsValid: true, 176 | }, 177 | } { 178 | t.Run(test.name, func(t *testing.T) { 179 | valid := tree.IsValid(test.path) 180 | switch { 181 | case test.expectPathIsValid && !valid: 182 | t.Errorf("%v: Expected valid, got invalid", test.path) 183 | case !test.expectPathIsValid && valid: 184 | t.Errorf("%v: Expected invalid, got valid", test.path) 185 | } 186 | }) 187 | } 188 | } 189 | 190 | func TestNormalizePath(t *testing.T) { 191 | for _, test := range []struct { 192 | path string 193 | expected string 194 | expectedError bool 195 | }{ 196 | { 197 | path: "/", 198 | expected: "root", 199 | }, 200 | { 201 | path: "/first", 202 | expected: "root/first", 203 | }, 204 | { 205 | path: "/first/second", 206 | expected: "root/first/second", 207 | }, 208 | { 209 | path: "root", 210 | expected: "root", 211 | }, 212 | { 213 | path: "root/first", 214 | expected: "root/first", 215 | }, 216 | { 217 | path: "root/first/second", 218 | expected: "root/first/second", 219 | }, 220 | { 221 | path: "first", 222 | expected: "first", 223 | }, 224 | { 225 | path: "first/second", 226 | expected: "first/second", 227 | }, 228 | { 229 | path: "", 230 | expected: "", 231 | }, 232 | { 233 | path: "first/", 234 | expected: "first", 235 | }, 236 | { 237 | path: "first/second/", 238 | expected: "first/second", 239 | }, 240 | { 241 | path: "/first/", 242 | expected: "root/first", 243 | }, 244 | { 245 | path: "/first/second/", 246 | expected: "root/first/second", 247 | }, 248 | { 249 | path: "//", 250 | expectedError: true, 251 | }, 252 | { 253 | path: "first//second", 254 | expectedError: true, 255 | }, 256 | } { 257 | t.Run(test.path, func(t *testing.T) { 258 | got, err := normalizePath(test.path) 259 | switch { 260 | case !test.expectedError && err != nil: 261 | t.Errorf("%v: expected `%v`, got error: %v", test.path, test.expected, err) 262 | case test.expectedError && err == nil: 263 | t.Errorf("%v: expected error, got `%v`", test.path, got) 264 | case got != test.expected: 265 | t.Errorf("%v: expected `%v`, got `%v`", test.path, test.expected, got) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | func TestGetTransformationIdentifier(t *testing.T) { 272 | tree := makeTree(t) 273 | for _, test := range []struct { 274 | name string 275 | path string 276 | expected string 277 | expectedError bool 278 | }{ 279 | { 280 | path: "/grandmother/aunt/cousin", 281 | expected: "cousin_t", 282 | }, 283 | { 284 | path: "root/grandmother/aunt/cousin", 285 | expected: "cousin_t", 286 | }, 287 | { 288 | path: "invalid", 289 | expectedError: true, 290 | }, 291 | } { 292 | t.Run(test.name, func(t *testing.T) { 293 | got, err := tree.GetTransformationIdentifier(test.path) 294 | switch { 295 | case !test.expectedError && err != nil: 296 | t.Errorf("GetTransformationIdentifier(%q): expected %q, got error: %v", test.path, test.expected, err) 297 | case test.expectedError && err == nil: 298 | t.Errorf("GetTransformationIdentifier(%q): expected error, got %q", test.path, got) 299 | case got != test.expected: 300 | t.Errorf("GetTransformationIdentifier(%q): expected %q, got %q", test.path, test.expected, got) 301 | } 302 | }) 303 | } 304 | } 305 | 306 | func makeTree(t *testing.T) OcTree { 307 | const mappingsFile = "../testdata/oc_tree_test_mappings.pb" 308 | mappings, err := utils.LoadMappings(mappingsFile) 309 | if err != nil { 310 | t.Fatalf("Error during test set up: %v", err) 311 | } 312 | tree, err := NewTree(mappings) 313 | if err != nil { 314 | t.Fatalf("Error during test set up: %v", err) 315 | } 316 | return tree 317 | } 318 | -------------------------------------------------------------------------------- /oparse/parser.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package oparse parses simple expressions in orismologer protos. 19 | Basic arithmetic, variables, function calls, string literals, nested expressions, and string 20 | concatenation are supported. 21 | Based on the version originally published at: 22 | https://github.com/alecthomas/participle/blob/master/_examples/expr/main.go 23 | */ 24 | package oparse 25 | 26 | import ( 27 | "errors" 28 | "fmt" 29 | "log" 30 | "math" 31 | "strings" 32 | 33 | "github.com/alecthomas/participle" 34 | "github.com/golang/glog" 35 | ) 36 | 37 | // Operator represents an arithmetic (or string interpolation) operator, eg: +. 38 | type Operator int 39 | 40 | const ( 41 | // OpMul represents a multiplication symbol (*). 42 | OpMul Operator = iota 43 | 44 | // OpDiv represents a division symbol (/). 45 | OpDiv 46 | 47 | // OpAdd represents an addition symbol (+). 48 | OpAdd 49 | 50 | // OpSub represents a subtraction symbol (-). 51 | OpSub 52 | ) 53 | 54 | var operatorMap = map[string]Operator{"+": OpAdd, "-": OpSub, "*": OpMul, "/": OpDiv} 55 | 56 | // Capture implements Participle's Capture interface. 57 | func (o *Operator) Capture(s []string) error { 58 | *o = operatorMap[s[0]] 59 | return nil 60 | } 61 | 62 | // Arg captures a function argument as an identifier optionally followed by a comma. 63 | type Arg struct { 64 | Value Expression `@@` // nolint: govet 65 | Separator *string `[ "," ]` 66 | } 67 | 68 | // Function captures a function call as an identifier followed by a matched pair of brackets which 69 | // contain 0 or more arguments. 70 | type Function struct { 71 | Name string `@Ident` 72 | Open string `"("` 73 | Args []*Arg `{ @@ }` 74 | Close string `")"` 75 | } 76 | 77 | // Value captures a value, which is either a literal of some kind (eg: a string or a number) or 78 | // something that evaluates to one (eg: a function call, or a nested expression). 79 | type Value struct { 80 | // NB: All numeric values will be represented as floats, to simplify parsing. 81 | Number *float64 `@(Float|Int)` 82 | StrLiteral *string `| @(String|Char)` 83 | Function *Function `| @@` 84 | Variable *string `| @Ident` 85 | Subexpression *Expression `| "(" @@ ")"` 86 | } 87 | 88 | // Factor captures a base and an exponent. 89 | type Factor struct { 90 | Base *Value `@@` 91 | Exponent *Value `[ "^" @@ ]` 92 | } 93 | 94 | // OpFactor captures a multiplication or division operator followed by a factor. 95 | type OpFactor struct { 96 | Operator Operator `@("*" | "/")` 97 | Factor *Factor `@@` 98 | } 99 | 100 | // Term captures a Factor followed by an OpFactor. 101 | type Term struct { 102 | Left *Factor `@@` 103 | Right []*OpFactor `{ @@ }` 104 | } 105 | 106 | // OpTerm captures a plus or minus operator followed by a term. 107 | type OpTerm struct { 108 | Operator Operator `@("+" | "-")` 109 | Term *Term `@@` 110 | } 111 | 112 | // Expression is the top level node in the grammar AST. It represents the complete expression to be 113 | // parsed and evaluated. 114 | type Expression struct { 115 | Left *Term `@@` 116 | Right []*OpTerm `{ @@ }` 117 | } 118 | 119 | // Functions for displaying parsed expressions. Useful for debugging. 120 | 121 | func (o Operator) String() string { 122 | switch o { 123 | case OpMul: 124 | return "*" 125 | case OpDiv: 126 | return "/" 127 | case OpSub: 128 | return "-" 129 | case OpAdd: 130 | return "+" 131 | } 132 | glog.Error("Got unsupported operator while parsing expression") 133 | return "?" 134 | } 135 | 136 | func (f *Function) String() string { 137 | var args []string 138 | for _, arg := range f.Args { 139 | args = append(args, arg.Value.String()) 140 | } 141 | return fmt.Sprintf("%v(%v)", f.Name, strings.Join(args, ", ")) 142 | } 143 | 144 | func (v *Value) String() string { 145 | switch { 146 | case v.Number != nil: 147 | return fmt.Sprintf("%g", *v.Number) 148 | case v.StrLiteral != nil: 149 | return fmt.Sprintf("%q", *v.StrLiteral) 150 | case v.Variable != nil: 151 | return *v.Variable 152 | case v.Function != nil: 153 | return v.Function.String() 154 | case v.Subexpression != nil: 155 | return "(" + v.Subexpression.String() + ")" 156 | default: 157 | return "" 158 | } 159 | } 160 | 161 | func (f *Factor) String() string { 162 | out := f.Base.String() 163 | if f.Exponent != nil { 164 | out += " ^ " + f.Exponent.String() 165 | } 166 | return out 167 | } 168 | 169 | func (o *OpFactor) String() string { 170 | return fmt.Sprintf("%s %s", o.Operator, o.Factor) 171 | } 172 | 173 | func (t *Term) String() string { 174 | out := []string{t.Left.String()} 175 | for _, r := range t.Right { 176 | out = append(out, r.String()) 177 | } 178 | return strings.Join(out, " ") 179 | } 180 | 181 | func (o *OpTerm) String() string { 182 | return fmt.Sprintf("%s %s", o.Operator, o.Term) 183 | } 184 | 185 | func (e *Expression) String() string { 186 | out := []string{e.Left.String()} 187 | for _, r := range e.Right { 188 | out = append(out, r.String()) 189 | } 190 | return strings.Join(out, " ") 191 | } 192 | 193 | // Functions for actually evaluating parsed expressions. 194 | 195 | func (o Operator) eval(l, r interface{}) (interface{}, error) { 196 | _, lIsInt := l.(int) 197 | _, rIsInt := r.(int) 198 | // Because of earlier handling we can assume that all numeric values are represented as floats. 199 | // ie: We should never get an int here. 200 | if lIsInt || rIsInt { 201 | log.Fatal("Evaluated parser output contained an int. That should not have happened.") 202 | } 203 | 204 | lFloat, lIsFloat := l.(float64) 205 | rFloat, rIsFloat := r.(float64) 206 | _, lIsString := l.(string) 207 | _, rIsString := r.(string) 208 | 209 | if lIsFloat && rIsFloat { 210 | // Accept loss in precision in exchange for simpler code by always using floats for arithmetic. 211 | switch o { 212 | case OpMul: 213 | return lFloat * rFloat, nil 214 | case OpDiv: 215 | if rFloat == 0 { 216 | return nil, errors.New("division by 0") 217 | } 218 | return lFloat / rFloat, nil 219 | case OpAdd: 220 | return lFloat + rFloat, nil 221 | case OpSub: 222 | return lFloat - rFloat, nil 223 | } 224 | return nil, errors.New(fmt.Sprintf("unsupported float operator: %v", o)) 225 | } 226 | 227 | if lIsString || rIsString { 228 | if o == OpAdd { 229 | return fmt.Sprint(l) + fmt.Sprint(r), nil 230 | } 231 | return nil, fmt.Errorf("unsupported string operator (use '+' for concatenation): %v", o) 232 | } 233 | 234 | return nil, errors.New("unsupported type (only floats and strings are supported)") 235 | } 236 | 237 | func (f *Function) eval(ctx Context, caller FunctionCaller) (interface{}, error) { 238 | var args []interface{} 239 | for _, arg := range f.Args { 240 | argEval, err := arg.Value.eval(ctx, caller) 241 | if err != nil { 242 | return nil, err 243 | } 244 | args = append(args, argEval) 245 | } 246 | result, err := caller(f.Name, args...) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | // Convert any int output to float, to simplify parsing. 252 | resultInt, resultIsInt := result.(int) 253 | if resultIsInt { 254 | return float64(resultInt), nil 255 | } 256 | return result, nil 257 | } 258 | 259 | func (v *Value) eval(ctx Context, caller FunctionCaller) (interface{}, error) { 260 | switch { 261 | case v.Number != nil: 262 | return *v.Number, nil 263 | case v.StrLiteral != nil: 264 | return *v.StrLiteral, nil 265 | case v.Variable != nil: 266 | value, ok := ctx[*v.Variable] 267 | if !ok { 268 | return nil, errors.New("no such variable " + *v.Variable) 269 | } 270 | // Attempt to cast to float, then string, then fail. 271 | valueInt, ok := value.(int) 272 | if ok { 273 | return float64(valueInt), nil 274 | } 275 | valueFloat, ok := value.(float64) 276 | if ok { 277 | return valueFloat, nil 278 | } 279 | valueString, ok := value.(string) 280 | if ok { 281 | return valueString, nil 282 | } 283 | return nil, fmt.Errorf("could not cast variable `%v` to float or string", *v.Variable) 284 | case v.Function != nil: 285 | return v.Function.eval(ctx, caller) 286 | case v.Subexpression != nil: 287 | return v.Subexpression.eval(ctx, caller) 288 | default: 289 | return nil, nil 290 | } 291 | } 292 | 293 | func (f *Factor) eval(ctx Context, caller FunctionCaller) (interface{}, error) { 294 | b, err := f.Base.eval(ctx, caller) 295 | if err != nil { 296 | return nil, err 297 | } 298 | 299 | if f.Exponent != nil { 300 | exponentEval, err := f.Exponent.eval(ctx, caller) 301 | if err != nil { 302 | return nil, err 303 | } 304 | return math.Pow(b.(float64), exponentEval.(float64)), nil 305 | } 306 | return b, nil 307 | } 308 | 309 | func (t *Term) eval(ctx Context, caller FunctionCaller) (interface{}, error) { 310 | n, err := t.Left.eval(ctx, caller) 311 | if err != nil { 312 | return nil, err 313 | } 314 | 315 | for _, r := range t.Right { 316 | rFactorEval, err := r.Factor.eval(ctx, caller) 317 | if err != nil { 318 | return nil, err 319 | } 320 | 321 | n, err = r.Operator.eval(n, rFactorEval) 322 | if err != nil { 323 | return nil, err 324 | } 325 | } 326 | return n, nil 327 | } 328 | 329 | func (e *Expression) eval(ctx Context, caller FunctionCaller) (interface{}, error) { 330 | l, err := e.Left.eval(ctx, caller) 331 | if err != nil { 332 | return nil, err 333 | } 334 | 335 | for _, r := range e.Right { 336 | rEval, err := r.Term.eval(ctx, caller) 337 | if err != nil { 338 | return nil, err 339 | } 340 | 341 | l, err = r.Operator.eval(l, rEval) 342 | if err != nil { 343 | return nil, err 344 | } 345 | } 346 | return l, nil 347 | } 348 | 349 | // Functions for returning information about expressions. 350 | 351 | func (f *Function) identifiers() (variables []string, functions []string) { 352 | functions = append(functions, f.Name) 353 | for _, arg := range f.Args { 354 | argVars, argFuncs := arg.Value.Identifiers() 355 | variables = append(variables, argVars...) 356 | functions = append(functions, argFuncs...) 357 | } 358 | return variables, functions 359 | } 360 | 361 | func (v *Value) identifiers() (variables []string, functions []string) { 362 | switch { 363 | case v.Variable != nil: 364 | variables = append(variables, *v.Variable) 365 | case v.Function != nil: 366 | return v.Function.identifiers() 367 | case v.Subexpression != nil: 368 | return v.Subexpression.Identifiers() 369 | } 370 | return variables, functions 371 | } 372 | 373 | func (f *Factor) identifiers() (variables []string, functions []string) { 374 | variables, functions = f.Base.identifiers() 375 | if f.Exponent != nil { 376 | expVars, expFuncs := f.Exponent.identifiers() 377 | variables = append(variables, expVars...) 378 | functions = append(functions, expFuncs...) 379 | } 380 | return variables, functions 381 | } 382 | 383 | func (t *Term) identifiers() (variables []string, functions []string) { 384 | variables, functions = t.Left.identifiers() 385 | for _, r := range t.Right { 386 | rFactorVars, rFactorFuncs := r.Factor.identifiers() 387 | variables = append(variables, rFactorVars...) 388 | functions = append(functions, rFactorFuncs...) 389 | } 390 | return variables, functions 391 | } 392 | 393 | // Identifiers returns the names of the variables and functions in the given expression. 394 | func (e *Expression) Identifiers() (variables []string, functions []string) { 395 | if e.Left != nil { // Can be nil if the expression is empty (ie: ""). 396 | variables, functions = e.Left.identifiers() 397 | } 398 | for _, r := range e.Right { 399 | opTermVars, opTermFuncs := r.Term.identifiers() 400 | variables = append(variables, opTermVars...) 401 | functions = append(functions, opTermFuncs...) 402 | } 403 | return variables, functions 404 | } 405 | 406 | // Context maps variable names to the values they should be replaced by in expressions. 407 | type Context map[string]interface{} 408 | 409 | /* 410 | FunctionCaller defines a function which can call another function given its name as a string and any 411 | arguments. 412 | */ 413 | type FunctionCaller func(string, ...interface{}) (interface{}, error) 414 | 415 | /* 416 | Parse is a convenience function which parses a string and returns the resulting expression, which 417 | can then be evaluated. 418 | */ 419 | func Parse(input string) (*Expression, error) { 420 | expression := &Expression{} 421 | parser, err := participle.Build(expression) 422 | if err != nil { 423 | return nil, fmt.Errorf("could not build parser (try checking the grammar): %v", err) 424 | } 425 | 426 | if err = parser.ParseString(input, expression); err != nil { 427 | return nil, fmt.Errorf("could not parse string %q: %v", input, err) 428 | } 429 | return expression, nil 430 | } 431 | 432 | /* 433 | Eval is a convenience function which evaluates a parsed expression and returns the result. 434 | The ctx parameter is a map containing variable definitions. Note that all numeric variable values 435 | are cast to float64. 436 | */ 437 | func Eval(expression *Expression, ctx Context, caller FunctionCaller) (interface{}, error) { 438 | result, err := expression.eval(ctx, caller) 439 | if err != nil { 440 | return nil, fmt.Errorf("could not evaluate expression `%v`: %v", expression, err) 441 | } 442 | glog.Infof("Evaluated expression: %v = %v", expression, result) 443 | return result, nil 444 | } 445 | -------------------------------------------------------------------------------- /oparse/parser_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package oparse 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/google/go-cmp/cmp" 23 | ) 24 | 25 | func TestParse(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | expressionString string 29 | expectedError bool 30 | }{ 31 | { 32 | name: "empty expression", 33 | expectedError: true, 34 | }, 35 | } 36 | for _, test := range tests { 37 | t.Run(test.name, func(t *testing.T) { 38 | _, err := Parse(test.expressionString) 39 | switch { 40 | case err == nil && test.expectedError: 41 | t.Errorf("%v: expected error, but got no error", test.name) 42 | case err != nil && !test.expectedError: 43 | t.Errorf("%v: got error: %v", test.name, err) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestEval(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | expressionString string 53 | context Context 54 | expected interface{} 55 | expectedError bool 56 | }{ 57 | // Arithmetic 58 | { 59 | name: "arithmetic", 60 | expressionString: "1+2*3+4/2", 61 | expected: 9.0, 62 | }, 63 | { 64 | name: "brackets", 65 | expressionString: "2*(3+1)", 66 | expected: 8.0, 67 | }, 68 | { 69 | name: "arithmetic starting with brackets", 70 | expressionString: "(10 + 1) * 1000", 71 | expected: 11000.0, 72 | }, 73 | { 74 | name: "division by zero", 75 | expressionString: "100 / 0", 76 | expectedError: true, 77 | }, 78 | { 79 | name: "indirect division by zero", 80 | expressionString: "100 / (1-1)", 81 | expectedError: true, 82 | }, 83 | 84 | // Variables 85 | { 86 | name: "solo variable", 87 | expressionString: "i", 88 | context: Context{"i": 10}, 89 | expected: 10.0, 90 | }, 91 | { 92 | name: "arithmetic with a variable", 93 | expressionString: "i*2+3", 94 | context: Context{"i": 10}, 95 | expected: 23.0, 96 | }, 97 | { 98 | name: "invalid variable", 99 | expressionString: "j*2+3", 100 | context: Context{"i": 10}, 101 | expectedError: true, 102 | }, 103 | { 104 | name: "variables starting with brackets", 105 | expressionString: "(boot_time + last_change_relative) * 1000", 106 | context: Context{"boot_time": 10, "last_change_relative": 5}, 107 | expected: 15000.0, 108 | }, 109 | 110 | // Strings 111 | { 112 | name: "string variable", 113 | expressionString: "i", 114 | context: Context{"i": "hello"}, 115 | expected: "hello", 116 | }, 117 | { 118 | name: "string with single quotes", 119 | expressionString: "'hello world'", 120 | expected: "hello world", 121 | }, 122 | { 123 | name: "string with double quotes", 124 | expressionString: "\"hello world\"", 125 | expected: "hello world", 126 | }, 127 | { 128 | name: "string without quotes", 129 | expressionString: "hello", 130 | expectedError: true, 131 | }, 132 | { 133 | name: "string with missing closing quote", 134 | expressionString: "'hello", 135 | expectedError: true, 136 | }, 137 | { 138 | name: "string with missing opening quote", 139 | expressionString: "hello'", 140 | expectedError: true, 141 | }, 142 | { 143 | name: "extraneous quote", 144 | expressionString: "'hello''", 145 | expectedError: true, 146 | }, 147 | { 148 | name: "empty string", 149 | expressionString: "''", 150 | expected: "", 151 | }, 152 | { 153 | name: "string concatenation", 154 | expressionString: "'hello' + ' ' + 'hello'", 155 | expected: "hello hello", 156 | }, 157 | { 158 | name: "string variable concatenation", 159 | expressionString: "i+' '+i", 160 | context: Context{"i": "hello"}, 161 | expected: "hello hello", 162 | }, 163 | { 164 | name: "String concatenation with numbers", 165 | expressionString: "'The answer is ' + 41 + 1", 166 | expected: "The answer is 411", 167 | }, 168 | { 169 | name: "String concatenation with brackets", 170 | expressionString: "'The answer is ' + (41 + 1)", 171 | expected: "The answer is 42", 172 | }, 173 | { 174 | name: "Invalid string concatenation", 175 | expressionString: "'The answer is ' * 2", 176 | expectedError: true, 177 | }, 178 | 179 | // Functions 180 | { 181 | name: "function call", 182 | expressionString: "myfunc()", 183 | expected: 1.0, 184 | }, 185 | { 186 | name: "function call with missing closing bracket", 187 | expressionString: "myfunc(", 188 | expectedError: true, 189 | }, 190 | { 191 | name: "function call with missing opening bracket", 192 | expressionString: "myfunc)", 193 | expectedError: true, 194 | }, 195 | { 196 | name: "function call with single, numeric parameter", 197 | expressionString: "myfunc(100)", 198 | expected: 1.0, 199 | }, 200 | { 201 | name: "function call with single, string parameter", 202 | expressionString: "myfunc('hello')", 203 | expected: 1.0, 204 | }, 205 | { 206 | name: "function call with multiple parameters", 207 | expressionString: "myfunc(1, 'hello')", 208 | expected: 1.0, 209 | }, 210 | { 211 | name: "function call with variable parameter", 212 | expressionString: "myfunc(i)", 213 | context: Context{"i": 999}, 214 | expected: 1.0, 215 | }, 216 | { 217 | name: "function call with nested expressions", 218 | expressionString: "myfunc('hello'+' there', 9/3, anotherfunc(2+4))", 219 | expected: 1.0, 220 | }, 221 | { 222 | name: "function call with arithmetic", 223 | expressionString: "myfunc(100) + 3 * i / 5", 224 | context: Context{"i": 10}, 225 | expected: 7.0, 226 | }, 227 | { 228 | name: "function call starting with brackets", 229 | expressionString: "(boot_time + to_int(last_change_relative)) * 1000", 230 | context: Context{"boot_time": 10, "last_change_relative": 5}, 231 | expected: 11000.0, 232 | }, 233 | { 234 | name: "function call with string concatenation", 235 | expressionString: "'The answer is ' + (41 + myfunc(100))", 236 | expected: "The answer is 42", 237 | }, 238 | } 239 | // Dummy function caller which returns 1 for any function name. 240 | caller := func(funcName string, args ...interface{}) (interface{}, error) { 241 | return 1, nil 242 | } 243 | for _, test := range tests { 244 | t.Run(test.name, func(t *testing.T) { 245 | expression, err := Parse(test.expressionString) 246 | var got interface{} 247 | if err == nil { 248 | got, err = Eval(expression, test.context, caller) 249 | } 250 | switch { 251 | case !test.expectedError && err != nil: 252 | t.Errorf("%v: got `%v`, expected no error", test.name, err) 253 | case test.expectedError && err == nil: 254 | t.Errorf("%v: got no error, expected error", test.name) 255 | case !cmp.Equal(test.expected, got) && err == nil: 256 | t.Errorf("%v: got `%v`, expected `%v`", test.name, got, test.expected) 257 | } 258 | }) 259 | } 260 | } 261 | 262 | func TestIdentifiers(t *testing.T) { 263 | tests := []struct { 264 | name string 265 | expressionString string 266 | expectedVars []string 267 | expectedFuncs []string 268 | }{ 269 | { 270 | name: "no identifiers", 271 | expressionString: "1 + 3 - 4", 272 | }, 273 | { 274 | name: "one variable", 275 | expressionString: "i", 276 | expectedVars: []string{"i"}, 277 | }, 278 | { 279 | name: "one func", 280 | expressionString: "func()", 281 | expectedFuncs: []string{"func"}, 282 | }, 283 | { 284 | name: "one func, one var", 285 | expressionString: "i + func()", 286 | expectedFuncs: []string{"func"}, 287 | expectedVars: []string{"i"}, 288 | }, 289 | { 290 | name: "var in func", 291 | expressionString: "func(i)", 292 | expectedFuncs: []string{"func"}, 293 | expectedVars: []string{"i"}, 294 | }, 295 | { 296 | name: "arithmetic containing a var in a func", 297 | expressionString: "func(i+1)", 298 | expectedFuncs: []string{"func"}, 299 | expectedVars: []string{"i"}, 300 | }, 301 | { 302 | name: "complex", 303 | expressionString: "i + j + func(s, t) * myfunc(q + another(1+3))", 304 | expectedFuncs: []string{"func", "myfunc", "another"}, 305 | expectedVars: []string{"i", "j", "s", "t", "q"}, 306 | }, 307 | { 308 | name: "start with a bracket", 309 | expressionString: "(boot_time + to_int(last_change_relative)) * 1000", 310 | expectedFuncs: []string{"to_int"}, 311 | expectedVars: []string{"boot_time", "last_change_relative"}, 312 | }, 313 | } 314 | for _, test := range tests { 315 | t.Run(test.name, func(t *testing.T) { 316 | expression, err := Parse(test.expressionString) 317 | gotVars, gotFuncs := expression.Identifiers() 318 | switch { 319 | case err != nil: 320 | t.Errorf("Identifiers(%q) got error: %v", test.expressionString, err) 321 | case !cmp.Equal(gotVars, test.expectedVars): 322 | t.Errorf("Identifiers(%q) got vars: %v; expected: %v", test.expressionString, gotVars, test.expectedVars) 323 | case !cmp.Equal(gotFuncs, test.expectedFuncs): 324 | t.Errorf("Identifiers(%q) got funcs: %v; expected: %v", test.expressionString, gotFuncs, test.expectedFuncs) 325 | } 326 | }) 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /orismologer/orismologer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package orismologer translates non-OpenConfig telemetry sources (eg: SNMP OIDs) to OpenConfig paths. 19 | */ 20 | package orismologer 21 | 22 | import ( 23 | "fmt" 24 | "strings" 25 | 26 | "github.com/golang/glog" 27 | "github.com/google/orismologer/functions" 28 | "github.com/google/orismologer/octree" 29 | "github.com/google/orismologer/oparse" 30 | "github.com/google/orismologer/utils" 31 | 32 | pb "github.com/google/orismologer/proto_out/proto" 33 | ) 34 | 35 | type transformationMap map[string]*pb.Transformation 36 | type nocPathResolver func(*pb.NocPath, string) (interface{}, error) 37 | type functionLibrary interface { 38 | Contains(funcName string) bool 39 | Call(funcName string, args ...interface{}) (interface{}, error) 40 | } 41 | 42 | // Orismologer translates non-OpenConfig telemetry sources (eg: SNMP OIDs) to OpenConfig paths. 43 | type Orismologer struct { 44 | mappings octree.OcTree 45 | transformations transformationMap 46 | vendorInfo *pb.VendorOids 47 | nocPathResolver nocPathResolver 48 | functions functionLibrary 49 | } 50 | 51 | /* 52 | NewOrismologer builds an Orismologer instance from the text protos in the given files. 53 | mappingsFile should contain a Mappings proto. 54 | transformationFile should contain a Transformations proto. 55 | vendorOidsFile should contain a VendorOids proto. 56 | */ 57 | func NewOrismologer(mappingsFile, transformationsFile, vendorOidsFile string) (*Orismologer, error) { 58 | mappings, err := utils.LoadMappings(mappingsFile) 59 | if err != nil { 60 | return nil, err 61 | } 62 | transformations, err := utils.LoadTransformations(transformationsFile) 63 | if err != nil { 64 | return nil, err 65 | } 66 | vendorOids, err := utils.LoadVendorOids(vendorOidsFile) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return newOrismologer(mappings, transformations, vendorOids) 71 | } 72 | 73 | func newOrismologer(mappings *pb.Mappings, transformations *pb.Transformations, vendorInfo *pb.VendorOids) (*Orismologer, error) { 74 | t, err := octree.NewTree(mappings) 75 | if err != nil { 76 | return nil, err 77 | } 78 | transformationMap, err := makeTransformationMap(transformations) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return &Orismologer{ 83 | mappings: t, 84 | transformations: transformationMap, 85 | vendorInfo: vendorInfo, 86 | nocPathResolver: resolve, 87 | functions: functions.NewLibrary(), 88 | }, nil 89 | } 90 | 91 | func makeTransformationMap(transformations *pb.Transformations) (transformationMap, error) { 92 | transformationMap := transformationMap{} 93 | for _, transformation := range transformations.GetTransformations() { 94 | name := transformation.GetBind() 95 | if _, ok := transformationMap[name]; ok { 96 | return nil, fmt.Errorf("more than one transformation bound to identifier %q", name) 97 | } 98 | transformationMap[name] = transformation 99 | } 100 | return transformationMap, nil 101 | } 102 | 103 | // PrintOcPaths pretty prints the tree of OpenConfig paths defined for this Orismologer instance. 104 | func (o *Orismologer) PrintOcPaths(root string) error { 105 | return o.mappings.Print(root) 106 | } 107 | 108 | /* 109 | Eval retrieves the current value of a given OpenConfig path for a target which does not natively 110 | support OpenConfig. 111 | The vendor name is used to identify dependencies for the target (eg: which OIDs it supports). 112 | */ 113 | // TODO: Support a dry run, to validate mappings and transformations protos. 114 | func (o *Orismologer) Eval(openConfigPath, target, vendor string) (interface{}, error) { 115 | transformationName, err := o.mappings.GetTransformationIdentifier(openConfigPath) 116 | if err != nil { 117 | return nil, fmt.Errorf("failed to identify a transformation for path %q: %v", openConfigPath, err) 118 | } 119 | transformation, ok := o.transformations[transformationName] 120 | if !ok { 121 | return nil, fmt.Errorf("could not locate transformation %q for path %q", transformationName, openConfigPath) 122 | } 123 | glog.Infof("found transformation %q for path %q", transformationName, openConfigPath) 124 | return o.eval(transformation, target, vendor) 125 | } 126 | 127 | /* 128 | eval parses and evaluates a Transformation proto's Expressions field, resolving any variables used 129 | in expressions to their associated Transformations and recursively evaluating those until a final 130 | value is obtained by resolving a NocPath. If a transformation defines multiple expressions then the 131 | output of the first one that successfully evaluates is returned. 132 | 133 | NocPaths are resolved using the function given to the Orismologer instance at instantiation. 134 | */ 135 | // TODO: Eval paths with keys, eg: thing/name[name=value] 136 | // TODO: Safeguard against really long paths, and circular references. 137 | func (o *Orismologer) eval(transformation *pb.Transformation, target string, vendor string) (interface{}, error) { 138 | transformationName := transformation.GetBind() 139 | glog.Infof("evaluating transformation %q for target %q of vendor %q", transformationName, target, vendor) 140 | nocPaths := o.getNocPaths(transformation) 141 | // Try to eval each expression defined for this transformation, taking the first that works. 142 | for _, expressionString := range transformation.GetExpressions() { 143 | glog.Infof("evaluating expression `%v`", expressionString) 144 | expression, variables, _, err := o.parseAndValidateExpression(expressionString) 145 | if err != nil { 146 | glog.Errorf("%v", err) 147 | continue 148 | } 149 | values, err := o.evalVariables(variables, nocPaths, target, vendor) 150 | if err != nil { 151 | if unresolvableNocPathError, ok := err.(unresolvableNocPathError); ok { 152 | glog.Info(unresolvableNocPathError.msg) // This is not an error we need to surface to the user. 153 | } else { 154 | glog.Errorf("%v", err) 155 | } 156 | glog.Infof("could not evaluate all variables for expression `%v`, continuing to next expression", expressionString) 157 | continue 158 | } 159 | 160 | // Evaluate the expression, passing in the values of the variables it uses. 161 | transformationResult, err := oparse.Eval(expression, values, o.functions.Call) 162 | if err != nil { 163 | return nil, err 164 | } 165 | return transformationResult, nil 166 | } 167 | return nil, fmt.Errorf("none of the expressions of transformation %q could be evaluated (see logs for details)", transformationName) 168 | } 169 | 170 | // getNocPaths returns a map of all the NocPaths defined in the given transformation. 171 | func (o *Orismologer) getNocPaths(transformation *pb.Transformation) map[string]*pb.NocPath { 172 | transformationName := transformation.GetBind() 173 | paths := map[string]*pb.NocPath{} 174 | for _, nocPath := range transformation.GetNocPaths() { 175 | pathName := nocPath.GetBind() 176 | if len(pathName) == 0 { 177 | glog.Errorf("Transformation %q contains a NocPath without an identifier", transformationName) 178 | } else { 179 | glog.Infof("storing NocPath %q of transformation %q", pathName, transformationName) 180 | paths[pathName] = nocPath 181 | } 182 | } 183 | return paths 184 | } 185 | 186 | /* 187 | Returns the expression parsed from the given string and any variables and function names used in it. 188 | */ 189 | func (o *Orismologer) parseAndValidateExpression(expressionString string) (*oparse.Expression, []string, []string, error) { 190 | expression, err := oparse.Parse(expressionString) 191 | if err != nil { 192 | glog.Errorf("could not parse expression `%v`", expressionString) 193 | return nil, nil, nil, err 194 | } 195 | variables, functionNames := expression.Identifiers() 196 | for _, functionName := range functionNames { 197 | if !o.functions.Contains(functionName) { 198 | return nil, nil, nil, fmt.Errorf("function %q is not defined", functionName) 199 | } 200 | } 201 | return expression, variables, functionNames, nil 202 | } 203 | 204 | /* 205 | Evaluates each of the given variables, returning an error if one or more cannot be evaluated. 206 | */ 207 | func (o *Orismologer) evalVariables(variables []string, nocPaths map[string]*pb.NocPath, target string, vendor string) (map[string]interface{}, error) { 208 | values := oparse.Context{} 209 | for _, variable := range variables { 210 | glog.Infof("evaluating variable %q", variable) 211 | var value interface{} 212 | var err error 213 | nocPath := nocPaths[variable] 214 | transformation := o.transformations[variable] 215 | switch { 216 | case nocPath != nil: 217 | value, err = o.handleNocPath(nocPath, target, vendor) 218 | if err != nil { 219 | return nil, err 220 | } 221 | case transformation != nil: 222 | value, err = o.eval(transformation, target, vendor) 223 | if err != nil { 224 | return nil, fmt.Errorf("could not evaluate sub-transformation %q: %v", variable, err) 225 | } 226 | default: 227 | return nil, fmt.Errorf("NocPath or sub-transformation %q is undefined", variable) 228 | } 229 | glog.Infof("evaluated variable %q = %v", variable, value) 230 | values[variable] = value 231 | } 232 | return values, nil 233 | } 234 | 235 | // Gets a value for the given NocPath for the given target. 236 | func (o *Orismologer) handleNocPath(nocPath *pb.NocPath, target string, vendor string) (interface{}, error) { 237 | pathName := nocPath.GetBind() 238 | if !o.canResolve(nocPath, vendor) { 239 | return nil, unresolvableNocPathError{ 240 | fmt.Sprintf("ignoring NocPath %q as it cannot be resolved for vendor %q", pathName, vendor), 241 | } 242 | } 243 | value, err := o.nocPathResolver(nocPath, target) 244 | if err != nil { 245 | return nil, fmt.Errorf("failed to resolve NocPath %q for target %q (this NocPath should normally be resolvable for this target): %v", pathName, target, err) 246 | } 247 | return value, nil 248 | } 249 | 250 | type unresolvableNocPathError struct { 251 | msg string 252 | } 253 | 254 | func (f unresolvableNocPathError) Error() string { 255 | return f.msg 256 | } 257 | 258 | // canResolve returns true if the given target supports the given NocPath. 259 | func (o *Orismologer) canResolve(nocPath *pb.NocPath, vendor string) bool { 260 | // NB: Currently assumes NocPaths are OIDs only. 261 | vendorRoot := o.vendorInfo.GetVendorRoot() 262 | for _, oid := range nocPath.GetOids() { 263 | if !strings.HasPrefix(oid, vendorRoot) { 264 | return true 265 | } 266 | vendorOid, ok := o.vendorInfo.GetVendors()[vendor] 267 | if !ok { 268 | return false 269 | } 270 | if strings.HasPrefix(oid, vendorRoot+"."+vendorOid) { 271 | return true 272 | } 273 | } 274 | return false 275 | } 276 | 277 | /* 278 | resolve retrieves the value for a given NocPath from a given target. 279 | This may involve sending an SNMP request, running a CLI command and parsing the output, etc. 280 | */ 281 | func resolve(nocPath *pb.NocPath, target string) (interface{}, error) { 282 | // TODO: Implement. 283 | glog.Infof("Requesting NocPath %q from target %q", nocPath.GetBind(), target) 284 | samples := nocPath.GetSamples() 285 | if len(samples) > 0 { 286 | return samples[0], nil 287 | } 288 | return "dummy", nil 289 | } 290 | -------------------------------------------------------------------------------- /orismologer/orismologer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package orismologer 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | "testing" 23 | 24 | "github.com/golang/glog" 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/google/orismologer/utils" 27 | 28 | pb "github.com/google/orismologer/proto_out/proto" 29 | ) 30 | 31 | func TestCanResolve(t *testing.T) { 32 | o, err := makeTestOrismologer() 33 | if err != nil { 34 | t.Fatalf("%v", err) 35 | } 36 | for _, test := range []struct { 37 | name string 38 | nocPath *pb.NocPath 39 | target string 40 | expected bool 41 | }{ 42 | { 43 | name: "eval-able", 44 | nocPath: &pb.NocPath{ 45 | Oids: []string{"1.3.6.1.4.1.9.9.48.1.1.1.5.1"}, 46 | }, 47 | target: "cisco", 48 | expected: true, 49 | }, 50 | { 51 | name: "un-eval-able", 52 | nocPath: &pb.NocPath{ 53 | Oids: []string{"1.3.6.1.4.1.9.9.48.1.1.1.5.1"}, 54 | }, 55 | target: "aruba", 56 | expected: false, 57 | }, 58 | { 59 | name: "invalid target", 60 | nocPath: &pb.NocPath{ 61 | Oids: []string{"1.3.6.1.4.1.9.9.48.1.1.1.5.1"}, 62 | }, 63 | target: "invalid", 64 | expected: false, 65 | }, 66 | { 67 | name: "multiple OIDs", 68 | nocPath: &pb.NocPath{ 69 | Oids: []string{ 70 | "1.3.6.1.4.1.9.9.48.1.1.1.5.1", 71 | "1.3.6.1.4.1.9.9.48.1.1.1.5.2", 72 | "1.3.6.1.4.1.14823.2.2.1.2.1.6", 73 | }, 74 | }, 75 | target: "aruba", 76 | expected: true, 77 | }, 78 | { 79 | name: "standard MIB for Cisco target", 80 | nocPath: &pb.NocPath{ 81 | Oids: []string{"1.3.6.1.2.1.25.3.3.1.2"}, 82 | }, 83 | target: "cisco", 84 | expected: true, 85 | }, 86 | { 87 | name: "standard MIB for Aruba target", 88 | nocPath: &pb.NocPath{ 89 | Oids: []string{"1.3.6.1.2.1.25.3.3.1.2"}, 90 | }, 91 | target: "aruba", 92 | expected: true, 93 | }, 94 | } { 95 | t.Run(test.name, func(t *testing.T) { 96 | if got, want := o.canResolve(test.nocPath, test.target), test.expected; got != want { 97 | t.Errorf("canResolve() = %v, expected %v", got, want) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func TestGetNocPaths(t *testing.T) { 104 | o, err := makeTestOrismologer() 105 | if err != nil { 106 | t.Fatalf("Could not set up test: %v", err) 107 | } 108 | for _, test := range []struct { 109 | name string 110 | transformation *pb.Transformation 111 | expectedPathNames []string 112 | }{ 113 | { 114 | name: "", 115 | transformation: &pb.Transformation{ 116 | Bind: "test", 117 | NocPaths: []*pb.NocPath{ 118 | {Bind: "noc_path_1"}, 119 | {}, 120 | {Bind: "noc_path_3"}, 121 | }, 122 | }, 123 | expectedPathNames: []string{"noc_path_1", "noc_path_3"}, 124 | }, 125 | { 126 | name: "", 127 | transformation: &pb.Transformation{ 128 | Bind: "test", 129 | NocPaths: []*pb.NocPath{ 130 | {Bind: "noc_path_1"}, 131 | {Bind: "noc_path_2"}, 132 | {Bind: "noc_path_3"}, 133 | }, 134 | }, 135 | expectedPathNames: []string{"noc_path_1", "noc_path_2", "noc_path_3"}, 136 | }, 137 | } { 138 | t.Run(test.name, func(t *testing.T) { 139 | paths := o.getNocPaths(test.transformation) 140 | var pathNames []string 141 | for _, path := range paths { 142 | pathNames = append(pathNames, path.GetBind()) 143 | } 144 | got := frequencyCounter(pathNames) 145 | expected := frequencyCounter(test.expectedPathNames) 146 | if !cmp.Equal(expected, got) { 147 | t.Fatalf("getNocPaths() = %v, expected %v", got, test.expectedPathNames) 148 | } 149 | }) 150 | } 151 | } 152 | 153 | func TestEval(t *testing.T) { 154 | o, err := makeTestOrismologer() 155 | if err != nil { 156 | t.Fatalf("Could not set up test: %v", err) 157 | } 158 | for _, test := range []struct { 159 | transformationName string 160 | vendor string 161 | expected interface{} 162 | expectsError bool 163 | }{ 164 | { 165 | transformationName: "cpu_name", 166 | vendor: "cisco", 167 | expectsError: true, 168 | }, 169 | { 170 | transformationName: "cpu_name", 171 | vendor: "aruba", 172 | expected: "Network Processor CPU10", 173 | }, 174 | { 175 | transformationName: "boot_time", 176 | vendor: "cisco", 177 | expected: 100.0, 178 | }, 179 | { 180 | transformationName: "boot_time", 181 | vendor: "aruba", 182 | expected: 100.0, 183 | }, 184 | { 185 | transformationName: "last_change_absolute", 186 | vendor: "cisco", 187 | expected: 150000.0, 188 | }, 189 | { 190 | transformationName: "last_change_absolute", 191 | vendor: "aruba", 192 | expected: 150000.0, 193 | }, 194 | } { 195 | testName := test.transformationName + "_" + test.vendor 196 | t.Run(testName, func(t *testing.T) { 197 | transformation := o.transformations[test.transformationName] 198 | got, err := o.eval(transformation, "target", test.vendor) 199 | switch { 200 | case err != nil && !test.expectsError: 201 | t.Errorf("eval(), got error: %v", err) 202 | case err == nil && test.expectsError: 203 | t.Errorf("eval(), expected error, got: %v", got) 204 | case err == nil && !test.expectsError && !cmp.Equal(got, test.expected): 205 | t.Errorf("eval() = %v, expected: %v", got, test.expected) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func makeTestOrismologer() (*Orismologer, error) { 212 | const transformationsFile = "../testdata/orismologer_test_transformations.pb" 213 | transformations, err := utils.LoadTransformations(transformationsFile) 214 | if err != nil { 215 | return nil, err 216 | } 217 | vendorInfo := &pb.VendorOids{ 218 | VendorRoot: "1.3.6.1.4.1", 219 | Vendors: map[string]string{ 220 | "cisco": "9", 221 | "aruba": "14823", 222 | }, 223 | } 224 | o, err := newOrismologer(&pb.Mappings{}, transformations, vendorInfo) 225 | if err != nil { 226 | return &Orismologer{}, fmt.Errorf("could not create Orismologer: %v", err) 227 | } 228 | o.nocPathResolver = func(nocPath *pb.NocPath, target string) (interface{}, error) { 229 | samples := nocPath.GetSamples() 230 | if len(samples) != 1 { 231 | glog.Errorf("NocPath in test data should include exactly one sample") 232 | return nil, nil 233 | } 234 | return samples[0], nil 235 | } 236 | o.functions = dummyLibrary{} 237 | return o, nil 238 | } 239 | 240 | func frequencyCounter(strings []string) map[string]int { 241 | counters := map[string]int{} 242 | for _, s := range strings { 243 | counters[s]++ 244 | } 245 | return counters 246 | } 247 | 248 | type dummyLibrary struct{} 249 | 250 | func (l dummyLibrary) Call(funcName string, args ...interface{}) (interface{}, error) { 251 | switch funcName { 252 | case "to_int": 253 | i, _ := strconv.Atoi(args[0].(string)) 254 | return i, nil 255 | case "to_string": 256 | return args[0].(string), nil 257 | case "time_since_epoch": 258 | return 20000100, nil 259 | default: 260 | return nil, fmt.Errorf("function %q undefined", funcName) 261 | } 262 | } 263 | 264 | func (l dummyLibrary) Contains(funcName string) (contains bool) { 265 | defer func() { 266 | if r := recover(); r != nil { 267 | contains = true 268 | } 269 | }() 270 | _, err := l.Call(funcName) 271 | contains = err == nil 272 | return contains 273 | } 274 | -------------------------------------------------------------------------------- /proto/mappings.pb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # proto-file: proto/mappings.proto 16 | # proto-message: Mappings 17 | 18 | nodes { 19 | subpath {path: "/components/component[name=name_value]"} 20 | 21 | map {key: "name_value", value: "index"} 22 | 23 | children { 24 | subpath: {path: "name"} 25 | bind: "cpu_name" 26 | } 27 | 28 | children { 29 | subpath {path: "cpu/utilization/state/avg"} 30 | bind: "avg_cpu_util" 31 | } 32 | } 33 | 34 | nodes { 35 | subpath {path: "/system"} 36 | 37 | children { 38 | subpath {path: "state/boot-time"} 39 | bind: "boot_time" 40 | } 41 | 42 | children { 43 | subpath {path: "memory"} 44 | 45 | children { 46 | subpath: {path: "state"} 47 | 48 | children { 49 | subpath: { 50 | # Total physical memory in bytes. 51 | path: "physical" 52 | revisions: "0.4.1" 53 | } 54 | bind: "total_memory_B" 55 | } 56 | 57 | children { 58 | subpath: { 59 | # Used memory in bytes. 60 | path: "reserved" 61 | revisions: "0.4.1" 62 | } 63 | bind: "used_memory" 64 | } 65 | } 66 | } 67 | } 68 | 69 | nodes { 70 | subpath { path: "/interfaces/interface[name=name_value]" } 71 | # NB: There does not appear to be a way to retrieve interface names via SNMP, so use indexes. 72 | map {key: "name_value", value: "interface_index"} 73 | 74 | children { 75 | subpath: {path: "state"} 76 | 77 | children { 78 | subpath: {path: "ifindex"} 79 | bind: "interface_index" 80 | } 81 | 82 | children { 83 | subpath: {path: "admin-status"} 84 | bind: "admin_status" 85 | } 86 | 87 | children { 88 | # The time at which the last system change occurred, in ns, relative to the Unix Epoch. 89 | subpath: {path: "last-change"} 90 | bind: "last_change_absolute" 91 | 92 | 93 | } 94 | 95 | children { 96 | subpath: {path: "counters"} 97 | 98 | children { 99 | subpath {path: "in-broadcast-pkts"} 100 | bind: "in_broadcast_packets" 101 | } 102 | 103 | children { 104 | subpath {path: "in-multicast-pkts"} 105 | bind: "in_multicast_packets" 106 | } 107 | 108 | children { 109 | subpath {path: "in-unicast-pkts"} 110 | bind: "in_unicast_packets" 111 | } 112 | 113 | children { 114 | subpath {path: "in-octets"} 115 | bind: "in_octets" 116 | } 117 | 118 | children { 119 | subpath {path: "out-broadcast-pkts"} 120 | bind: "out_broadcast_packets" 121 | } 122 | 123 | children { 124 | subpath {path: "out-multicast-pkts"} 125 | bind: "out_multicast_packets" 126 | } 127 | 128 | children { 129 | subpath {path: "out-unicast-pkts"} 130 | bind: "out_unicast_packets" 131 | } 132 | 133 | children { 134 | subpath {path: "out-octets"} 135 | bind: "out_octets" 136 | } 137 | 138 | children { 139 | subpath {path: "in-discards"} 140 | bind: "in_discards" 141 | } 142 | 143 | children { 144 | subpath {path: "in-errors"} 145 | bind: "in_errors" 146 | } 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /proto/mappings.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | package mappings; 19 | import "proto/paths.proto"; 20 | 21 | /* 22 | Top level message containing all transformations/defined variables. 23 | */ 24 | // TODO: Validate: NocPaths should not be redefined. 25 | message Transformations { 26 | repeated Transformation transformations = 1; 27 | } 28 | 29 | /* 30 | Top level message containing all mappings between OpenConfig paths and 31 | non-OpenConfig paths (NocPaths). 32 | */ 33 | message Mappings { 34 | // Represents nodes in an OpenConfig model. 35 | repeated OpenConfigNode nodes = 1; 36 | } 37 | 38 | /* 39 | Top level message mapping parts of the MIB tree to known vendors. 40 | */ 41 | message VendorOids { 42 | string vendor_root = 1; 43 | map vendors = 2; 44 | } 45 | 46 | /* 47 | Stores an OpenConfig path (and potentially some of its subpaths) and all the 48 | ways valid values for that path can be retrieved from non-OpenConfig paths. 49 | */ 50 | message OpenConfigNode { 51 | /* 52 | A valid OpenConfig sub-path. 53 | The full path is formed by recursively concatenating the sub-paths of 54 | nested node messages. 55 | Paths may optionally specify the OpenConfig revisions for which they are 56 | valid. 57 | */ 58 | // TODO: Validate OC path; a path in a top level node must start 59 | // from root (/) 60 | // TODO: Validate. If subpath contains index parameter 61 | // (/path[param=value]) this index param must be mapped in the map field. 62 | OpenConfigPath subpath = 1; 63 | 64 | /* 65 | Specifies a variable (by its identifier) which produces valid output for the 66 | OpenConfig subpath of this node. 67 | */ 68 | // TODO: Validate that value is a declared identifier 69 | // (in scope). 70 | string bind = 2; 71 | 72 | /* 73 | Tells message consumers to bind a variable in an OpenConfig path to a 74 | variable in a non-OpenConfig path. 75 | Eg: An OC path might be given as "/components/component[name=value] and an OID 76 | might be "1.3.index. Binding `value` to `index` means that the same value 77 | one should always correspond to the same value for the other (eg: name 78 | 'cpu_1' should always correspond to index '1'. 79 | */ 80 | map map = 3; 81 | 82 | // Represents the children of this node. 83 | repeated OpenConfigNode children = 4; 84 | } 85 | 86 | // Represents a function which transforms data (eg: to OpenConfig format). 87 | message Transformation { 88 | /* 89 | The identifier to which the transformation is bound. 90 | Can be referenced in expressions via this value. 91 | */ 92 | // TODO: Validate: no space or -, at least one non-numeric char 93 | string bind = 1; 94 | 95 | // Represents processing applied to variables. 96 | // TODO: Validate: Expression compiles, variables have been 97 | // defined etc. 98 | repeated string expressions = 2; 99 | 100 | repeated NocPath noc_paths = 3; 101 | } 102 | -------------------------------------------------------------------------------- /proto/paths.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | package mappings; 19 | 20 | // Represents an OpenConfig path. 21 | message OpenConfigPath { 22 | // TODO: Validate. Eg: Ensure path string is a real OC path. 23 | // See https://github.com/openconfig/reference/blob/master/rpc/gnmi/ 24 | // gnmi-path-strings.md. 25 | string path = 1; 26 | 27 | /* 28 | Specifies the revisions for which the path is valid. If no revisions are 29 | given then the path is assumed to be valid for all revisions. 30 | */ 31 | // TODO: Validate: Properly formatted semantic versioning, and 32 | // revision exists 33 | repeated string revisions = 2; 34 | } 35 | 36 | /* 37 | Represents a non-OpenConfig telemetry path (eg: an OID). If more than one 38 | path is given then they are assumed to be perfectly equivalent (without 39 | processing/transformation). 40 | */ 41 | message NocPath { 42 | string bind = 1; 43 | 44 | /* 45 | All paths given here are equivalent. Eg: OIDs for different vendors which 46 | yield identical output. 47 | Clients are expected to use only one of the given paths. 48 | Paths specified earlier in the message will be preferred over those 49 | specified later. 50 | */ 51 | // TODO: Validation. Ensure path string is a real SNMP path. 52 | repeated string oids = 2; // Dot-notation OID path. 53 | 54 | /* 55 | Optional sample output from the NocPath for use by maintainers and for 56 | automated testing. 57 | */ 58 | repeated string samples = 4; 59 | 60 | // Additional path types could be specified here, eg: format strings which 61 | // match CLI output. 62 | } 63 | 64 | /* 65 | Data types modeled by NocPaths. 66 | Data will be typecast as appropriate after being retrieved. 67 | */ 68 | enum DataType { 69 | UNDEFINED = 0; 70 | INT = 1; 71 | UINT = 2; 72 | FLOAT = 3; 73 | 74 | STRING = 4; 75 | 76 | ISO8601 = 5; 77 | NTP = 6; 78 | } 79 | -------------------------------------------------------------------------------- /proto/transformations.pb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # proto-file: proto/mappings.proto 16 | # proto-message: Transformations 17 | 18 | transformations { 19 | bind: "used_memory_cisco" 20 | expressions: "to_int(used_memory_B_cisco)" 21 | 22 | noc_paths { 23 | bind: "used_memory_b_cisco" 24 | # Used memory in B. 25 | # Index .1 corresponds to "processor" memory (processes). .2 is "I/O" (packet queues etc.) 26 | oids: "1.3.6.1.4.1.9.9.48.1.1.1.5.1" # Cisco: {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 9 ciscoMgmt(9) ciscoMemoryPoolMIB(48) ciscoMemoryPoolObjects(1) ciscoMemoryPoolTable(1) ciscoMemoryPoolEntry(1) ciscoMemoryPoolUsed(5)} 27 | samples: "383014872" 28 | } 29 | } 30 | 31 | transformations { 32 | # Time at which the system booted, in seconds, relative to the Linux Epoch. 33 | bind: "boot_time" 34 | expressions: "time_since_epoch(system_time_aruba, '2006-01-02 15:04:05', 's') - system_up_time" 35 | expressions: "time_since_epoch(system_time_cisco, 'ntp', 's') - system_up_time" 36 | 37 | noc_paths { 38 | bind: "system_time_aruba" 39 | oids: "1.3.6.1.4.1.14823.2.2.1.2.1.6" # Aruba: {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 14823 arubaEnterpriseMibModules(2) switch(2) wlsxEnterpriseMibModules(1) wlsxSystemExtMIB(2) wlsxSystemExtGroup(1) wlsxSysExtSwitchDate(6)} 40 | samples: "2018-12-18 15:15:59" 41 | } 42 | noc_paths { 43 | bind: "system_time_cisco" 44 | oids: "1.3.6.1.4.1.9.9.168.1.1.10" # Cisco. 45 | samples: "dfc4 0b68 8147 af78" # NTP timestamp (64bit, first 32b = seconds since 1900-01-01, second 32b = fractional component) 46 | } 47 | } 48 | 49 | transformations { 50 | # The number of seconds since the system booted. 51 | bind: "system_up_time" 52 | expressions: "to_int(system_up_time_100) / 100" 53 | 54 | noc_paths { 55 | # Time since system booted in 100ths of a second. 56 | bind: "system_up_time_100" 57 | oids: "1.3.6.1.2.1.1.3" # Standard MIB: {iso(1) identified-organization(3) dod(6) internet(1) mgmt(2) mib-2(1) system(1) sysUpTime(3)} 58 | samples: "2026708237" 59 | } 60 | } 61 | 62 | transformations { 63 | bind: "cpu_name" 64 | expressions: "cpu_name_aruba_oid" 65 | 66 | noc_paths { 67 | bind: "cpu_name_aruba_oid" 68 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.9.1.2.index" # Aruba: {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 14823 arubaEnterpriseMibModules(2) switch(2) wlsxEnterpriseMibModules(1) wlsxSwitchMIB(1) wlsxSystemXGroup(1) wlsxSysXProcessorTable(9) wlsxSysXProcessorEntry(1) sysXProcessorDescr(2)} 69 | samples: "Network Processor CPU10" 70 | } 71 | } 72 | 73 | transformations { 74 | bind: "avg_cpu_util" 75 | expressions: "to_int(avg_cpu_util_oid)" 76 | 77 | noc_paths { 78 | bind: "avg_cpu_util_oid" 79 | # CPU utilisation as a percentage, averaged over the past minute. 80 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.9.1.3.index" # Aruba: {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 14823 arubaEnterpriseMibModules(2) switch(2) wlsxEnterpriseMibModules(1) wlsxSwitchMIB(1) wlsxSystemXGroup(1) wlsxSysXProcessorTable(9) wlsxSysXProcessorEntry(1) sysXProcessorLoad(3)} 81 | oids: "1.3.6.1.4.1.9.9.109.1.1.1.1.7.index" # Cisco: {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 9 ciscoMgmt(9) ciscoProcessMIB(109) ciscoProcessMIBObjects(1) cpmCPU(1) cpmCPUTotalTable(1) cpmCPUTotalEntry(1) cpmCPUTotal1minRev(7)} 82 | oids: "1.3.6.1.4.1.2636.3.1.13.1.8.index" # Juniper TODO: Cannot find good reference to confirm that this is the right OID. 83 | oids: "1.3.6.1.2.1.25.3.3.1.2.index" # Standard MIB: {iso(1) identified-organization(3) dod(6) internet(1) mgmt(2) mib-2(1) host(25) hrDevice(3) hrProcessorTable(3) hrProcessorEntry(1) hrProcessorLoad(2)} 84 | samples: "6" 85 | samples: "19" 86 | samples: "0" 87 | samples: "100" 88 | } 89 | } 90 | 91 | transformations { 92 | bind: "total_memory_B" 93 | expressions: "to_int(total_memory_aruba) * 1000" 94 | expressions: "int(free_memory_cisco) + used_memory_cisco" 95 | 96 | noc_paths { 97 | bind: "total_memory_aruba" 98 | # Total memory in KB (Aruba). 99 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.11.1.2" # {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 14823 arubaEnterpriseMibModules(2) switch(2) wlsxEnterpriseMibModules(1) wlsxSwitchMIB(1) wlsxSystemXGroup(1) wlsxSysXMemoryTable(11) wlsxSysXMemoryEntry(1) sysXMemorySize(2)} 100 | samples: "5172096" 101 | } 102 | noc_paths { 103 | bind: "free_memory_cisco" 104 | # Free memory in bytes (Cisco). 105 | # Index .1 corresponds to "processor" memory (processes). .2 is "I/O" (packet queues etc.) 106 | oids: "1.3.6.1.4.1.9.9.48.1.1.1.6.1" # {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 9 ciscoMgmt(9) ciscoMemoryPoolMIB(48) ciscoMemoryPoolObjects(1) ciscoMemoryPoolTable(1) ciscoMemoryPoolEntry(1) ciscoMemoryPoolFree(6)} 107 | samples: "556513160" 108 | } 109 | } 110 | 111 | transformations { 112 | bind: "used_memory" 113 | expressions: "used_memory_cisco" 114 | expressions: "to_int(used_memory_KB_oid_aruba) * 1000" 115 | 116 | noc_paths { 117 | # Used memory in KB. 118 | bind: "used_memory_KB_oid_aruba" 119 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.11.1.3" # {iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 14823 arubaEnterpriseMibModules(2) switch(2) wlsxEnterpriseMibModules(1) wlsxSwitchMIB(1) wlsxSystemXGroup(1) wlsxSysXMemoryTable(11) wlsxSysXMemoryEntry(1) sysXMemoryUsed(3)} 120 | samples: "2179200" 121 | } 122 | } 123 | 124 | transformations { 125 | bind: "interface_index" 126 | expressions: "to_int(interface_index_raw)" 127 | 128 | noc_paths { 129 | bind: "interface_index_raw" 130 | oids: "1.3.6.1.2.1.2.2.1.1.interface_index" 131 | samples: "1" 132 | samples: "134217728" 133 | } 134 | } 135 | 136 | transformations { 137 | bind: "admin_status" 138 | expressions: "to_int(admin_status_raw)" 139 | 140 | noc_paths { 141 | bind: "admin_status_raw" 142 | oids: "1.3.6.1.2.1.2.2.1.7.interface_index" # Standard MIB. 143 | samples: "1" # Up. 144 | samples: "2" # Down. 145 | samples: "3" # Testing. 146 | } 147 | } 148 | 149 | transformations: { 150 | bind: "last_change_absolute" # Relative to Unix epoch. 151 | expressions: "(boot_time + to_int(last_change_relative)) * 1000000000" 152 | 153 | noc_paths { 154 | bind: "last_change_relative" # Seconds since system booted, when last change occurred. 155 | oids: "1.3.6.1.2.1.2.2.1.9.interface_index" # Standard MIB. 156 | samples: "1557122348" 157 | samples: "9624" 158 | } 159 | } 160 | 161 | transformations { 162 | bind: "in_broadcast_packets" 163 | expressions: "to_int(in_broadcast_packets_raw)" 164 | noc_paths { 165 | bind: "in_broadcast_packets_raw" 166 | oids: "1.3.6.1.2.1.31.1.1.1.9" # Standard MIB. 167 | samples: "0" 168 | samples: "70908519" 169 | } 170 | } 171 | 172 | transformations { 173 | bind: "in_multicast_packets" 174 | expressions: "to_int(in_multicast_packets_raw)" 175 | noc_paths { 176 | bind: "in_multicast_packets_raw" 177 | oids: "1.3.6.1.2.1.31.1.1.1.8" # Standard MIB. 178 | samples: "0" 179 | samples: "402079500" 180 | } 181 | } 182 | 183 | transformations { 184 | bind: "in_unicast_packets" 185 | expressions: "to_int(in_unicast_packets_raw)" 186 | noc_paths { 187 | bind: "in_unicast_packets_raw" 188 | oids: "1.3.6.1.2.1.31.1.1.1.7" # Standard MIB. 189 | samples: "0" 190 | samples: "154801769674" 191 | } 192 | } 193 | 194 | transformations { 195 | bind: "in_octets" 196 | expressions: "to_int(in_octets_raw)" 197 | noc_paths { 198 | bind: "in_octets_raw" 199 | oids: "1.3.6.1.2.1.31.1.1.1.6" # Standard MIB. 200 | samples: "0" 201 | samples: "128712049996217" 202 | } 203 | } 204 | 205 | transformations { 206 | bind: "out_broadcast_packets" 207 | expressions: "to_int(out_broadcast_packets_raw)" 208 | noc_paths { 209 | bind: "out_broadcast_packets_raw" 210 | oids: "1.3.6.1.2.1.31.1.1.1.13" # Standard MIB. 211 | samples: "0" 212 | samples: "72596133" 213 | } 214 | } 215 | 216 | transformations { 217 | bind: "out_multicast_packets" 218 | expressions: "to_int(out_multicast_packets_raw)" 219 | noc_paths { 220 | bind: "out_multicast_packets_raw" 221 | oids: "1.3.6.1.2.1.31.1.1.1.12" # Standard MIB. 222 | samples: "0" 223 | samples: "25298767" 224 | } 225 | } 226 | 227 | transformations { 228 | bind: "out_unicast_packets" 229 | expressions: "to_int(out_unicast_packets_raw)" 230 | noc_paths { 231 | bind: "out_unicast_packets_raw" 232 | oids: "1.3.6.1.2.1.31.1.1.1.11" # Standard MIB. 233 | samples: "0" 234 | samples: "153242010823" 235 | } 236 | } 237 | 238 | transformations { 239 | bind: "out_octets" 240 | expressions: "to_int(out_octets_raw)" 241 | noc_paths { 242 | bind: "out_octets_raw" 243 | oids: "1.3.6.1.2.1.31.1.1.1.10" # Standard MIB. 244 | samples: "0" 245 | samples: "130289305778248" 246 | } 247 | } 248 | 249 | transformations { 250 | bind: "in_discards" 251 | expressions: "to_int(in_discards_raw)" 252 | noc_paths { 253 | bind: "in_discards_raw" 254 | oids: "1.3.6.1.2.1.2.2.1.13" # Standard MIB. 255 | samples: "0" 256 | } 257 | } 258 | 259 | transformations { 260 | bind: "in_errors" 261 | expressions: "to_int(in_errors_raw)" 262 | noc_paths { 263 | bind: "in_errors_raw" 264 | oids: "1.3.6.1.2.1.2.2.1.14" # Standard MIB. 265 | samples: "0" 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /proto/vendor_oids.pb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # proto-file: proto/mappings.proto 16 | # proto-message: VendorOids 17 | 18 | vendor_root: "1.3.6.1.4.1" 19 | 20 | vendors { key: "cisco" value: "9" } 21 | vendors { key: "aruba" value: "14823" } 22 | -------------------------------------------------------------------------------- /testdata/oc_tree_test_mappings.pb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # proto-file: proto/mappings.proto 16 | # proto-message: Mappings 17 | 18 | nodes { 19 | subpath { path: "/paternal_grandfather" } 20 | 21 | children { 22 | subpath { path: "father" } 23 | 24 | children { 25 | subpath { path: "child" } 26 | } 27 | 28 | children { 29 | subpath { path: "sibling" } 30 | } 31 | } 32 | 33 | children { 34 | subpath { path: "paternal_aunt" } 35 | } 36 | } 37 | 38 | nodes { 39 | subpath { path: "/grandmother" } 40 | 41 | children { 42 | subpath {path: "aunt/cousin"} 43 | bind: "cousin_t" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /testdata/orismologer_test_transformations.pb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # proto-file: proto/mappings.proto 16 | # proto-message: Transformations 17 | 18 | transformations { 19 | bind: "cpu_name" 20 | expressions: "cpu_name_aruba_oid" 21 | 22 | noc_paths { 23 | bind: "cpu_name_aruba_oid" 24 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.9.1.2.index" 25 | samples: "Network Processor CPU10" 26 | } 27 | } 28 | 29 | ###################################### 30 | 31 | transformations { 32 | bind: "boot_time" 33 | expressions: "time_since_epoch(system_time_aruba, '2006-01-02 15:04:05', 's') - system_up_time" 34 | expressions: "time_since_epoch(system_time_cisco, 'ntp', 's') - system_up_time" 35 | 36 | noc_paths { 37 | bind: "system_time_aruba" 38 | oids: "1.3.6.1.4.1.14823.2.2.1.2.1.6" 39 | samples: "2018-12-18 15:15:59" 40 | } 41 | noc_paths { 42 | bind: "system_time_cisco" 43 | oids: "1.3.6.1.4.1.9.9.168.1.1.10" 44 | samples: "dfc4 0b68 8147 af78" 45 | } 46 | } 47 | 48 | transformations: { 49 | bind: "last_change_absolute" 50 | expressions: "(boot_time + to_int(last_change_relative)) * 1000" 51 | 52 | noc_paths { 53 | bind: "last_change_relative" 54 | oids: "1.3.6.1.2.1.2.2.1.9.interface_index" 55 | samples: "50" 56 | } 57 | } 58 | 59 | transformations { 60 | bind: "system_up_time" 61 | expressions: "to_int(system_up_time_100) / 100" 62 | 63 | noc_paths { 64 | bind: "system_up_time_100" 65 | oids: "1.3.6.1.2.1.1.3" 66 | samples: "2000000000" 67 | } 68 | } 69 | 70 | ###################################### 71 | 72 | transformations { 73 | bind: "used_memory" 74 | expressions: "used_memory_cisco" 75 | expressions: "to_int(used_memory_KB_oid_aruba) * 1000" 76 | 77 | noc_paths { 78 | bind: "used_memory_KB_oid_aruba" 79 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.11.1.3" 80 | samples: "2179200" 81 | } 82 | } 83 | 84 | transformations { 85 | bind: "total_memory_B" 86 | expressions: "to_int(total_memory_aruba) * 1000" 87 | expressions: "int(free_memory_cisco) + used_memory_cisco" 88 | 89 | noc_paths { 90 | bind: "total_memory_aruba" 91 | oids: "1.3.6.1.4.1.14823.2.2.1.1.1.11.1.2" 92 | samples: "5172096" 93 | } 94 | noc_paths { 95 | bind: "free_memory_cisco" 96 | oids: "1.3.6.1.4.1.9.9.48.1.1.1.6.1" 97 | samples: "556513160" 98 | } 99 | } 100 | 101 | transformations { 102 | bind: "used_memory_cisco" 103 | expressions: "to_int(used_memory_B_cisco)" 104 | 105 | noc_paths { 106 | bind: "used_memory_b_cisco" 107 | oids: "1.3.6.1.4.1.9.9.48.1.1.1.5.1" 108 | samples: "383014872" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package utils provides miscellaneous utilities for Orismologer. 18 | package utils 19 | 20 | import ( 21 | "fmt" 22 | "io/ioutil" 23 | "strings" 24 | 25 | "github.com/golang/protobuf/proto" 26 | 27 | pb "github.com/google/orismologer/proto_out/proto" 28 | ) 29 | 30 | // LoadMappings deserializes a text proto file at a given path as a Mappings proto message. 31 | func LoadMappings(mappingsFile string) (*pb.Mappings, error) { 32 | bytes, err := ioutil.ReadFile(mappingsFile) 33 | if err != nil { 34 | return nil, fmt.Errorf("could not open mappings file: %v", err) 35 | } 36 | mappings := &pb.Mappings{} 37 | if err := proto.UnmarshalText(string(bytes), mappings); err != nil { 38 | return nil, fmt.Errorf("could not deserialize mappings: %v", err) 39 | } 40 | return mappings, nil 41 | } 42 | 43 | // LoadTransformations deserializes a text proto file at a given path as a Transformations proto 44 | // message. 45 | func LoadTransformations(transformationsFile string) (*pb.Transformations, error) { 46 | bytes, err := ioutil.ReadFile(transformationsFile) 47 | if err != nil { 48 | return nil, fmt.Errorf("could not open transformations file: %v", err) 49 | } 50 | transformations := &pb.Transformations{} 51 | if err := proto.UnmarshalText(string(bytes), transformations); err != nil { 52 | return nil, fmt.Errorf("could not deserialize transformations: %v", err) 53 | } 54 | return transformations, nil 55 | } 56 | 57 | // LoadVendorOids deserializes a text proto file at a given path as a VendorOids proto message. 58 | func LoadVendorOids(vendorOidsFile string) (*pb.VendorOids, error) { 59 | bytes, err := ioutil.ReadFile(vendorOidsFile) 60 | if err != nil { 61 | return nil, fmt.Errorf("could not open vendor OIDs file: %v", err) 62 | } 63 | vendorOids := &pb.VendorOids{} 64 | if err := proto.UnmarshalText(string(bytes), vendorOids); err != nil { 65 | return nil, fmt.Errorf("could not deserialize vendor OIDs: %v", err) 66 | } 67 | return vendorOids, nil 68 | } 69 | 70 | // SliceToString returns a comma-separated string representing the contents of a slice. 71 | func SliceToString(slice []interface{}) string { 72 | valueStrings := make([]string, len(slice)) 73 | for i, value := range slice { 74 | valueStrings[i] = fmt.Sprint(value) 75 | } 76 | return strings.Join(valueStrings, ", ") 77 | } 78 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package utils provides miscellaneous utilities for Orismologer. 18 | package utils 19 | 20 | import "testing" 21 | 22 | func TestSliceToString(t *testing.T) { 23 | for _, test := range []struct { 24 | name string 25 | slice []interface{} 26 | expected string 27 | }{ 28 | { 29 | name: "simple", 30 | slice: []interface{}{"one", "two"}, 31 | expected: "one, two", 32 | }, 33 | { 34 | name: "empty", 35 | slice: []interface{}{}, 36 | expected: "", 37 | }, 38 | } { 39 | t.Run(test.name, func(t *testing.T) { 40 | if got := SliceToString(test.slice); got != test.expected { 41 | t.Errorf("SliceToString() = %v, expected %v", got, test.expected) 42 | } 43 | }) 44 | } 45 | } 46 | --------------------------------------------------------------------------------