Skip to content

Commit

Permalink
Parse inspected console item definition (#212)
Browse files Browse the repository at this point in the history
* parse output of /console/inspect command to populate resource fields

* update README

* generate mikrotik resource with pre-filled list of fields

* query resource fields from remote system
  • Loading branch information
maksym-nazarenko authored Feb 9, 2024
1 parent e3c0d79 commit 326ca07
Show file tree
Hide file tree
Showing 11 changed files with 477 additions and 5 deletions.
62 changes: 62 additions & 0 deletions client/console-inspected/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package consoleinspected

import (
"fmt"
"strings"
)

// Parse parses definition of inspected console item and extracts items using splitStrategy.
//
// It returns console item struct with its subcommands, commands, arguments, etc.
func Parse(input string, splitStrategy ItemsDefinitionSplitStrategy) (ConsoleItem, error) {
chunks, err := splitStrategy.Split(input)
if err != nil {
return ConsoleItem{}, err
}

var result ConsoleItem
result.Self = Item{}

for _, v := range chunks {
item, err := parseItem(v)
if err != nil {
return ConsoleItem{}, err
}
if item.Type == TypeSelf {
result.Self = item
continue
}
switch t := item.NodeType; t {
case NodeTypeDir:
result.Subcommands = append(result.Subcommands, item.Name)
case NodeTypeArg:
result.Arguments = append(result.Arguments, item)
case NodeTypeCommand:
result.Commands = append(result.Commands, item.Name)
default:
return ConsoleItem{}, fmt.Errorf("unknown node type %q", t)
}
}

return result, nil
}

func parseItem(input string) (Item, error) {
result := Item{}
for _, v := range strings.Split(input, ";") {
if strings.TrimSpace(v) == "" {
continue
}
if strings.HasPrefix(v, "name=") {
result.Name = strings.TrimPrefix(v, "name=")
}
if strings.HasPrefix(v, "node-type=") {
result.NodeType = NodeType(strings.TrimPrefix(v, "node-type="))
}
if strings.HasPrefix(v, "type=") {
result.Type = Type(strings.TrimPrefix(v, "type="))
}
}

return result, nil
}
65 changes: 65 additions & 0 deletions client/console-inspected/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package consoleinspected

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParse(t *testing.T) {
testCases := []struct {
name string
input string
expected ConsoleItem
expectedError bool
}{
{
name: "simple command",
input: "name=add;node-type=cmd;type=self;name=comment;node-type=arg;type=child;name=copy-from;node-type=arg;type=child;",
expected: ConsoleItem{
Self: Item{
Name: "add",
NodeType: NodeTypeCommand,
Type: TypeSelf,
},
Arguments: []Item{
{Name: "comment", NodeType: NodeTypeArg, Type: TypeChild},
{Name: "copy-from", NodeType: NodeTypeArg, Type: TypeChild},
},
},
},
{
name: "command with subcommands",
input: "name=list;node-type=dir;type=self;name=add;node-type=cmd;type=child;name=comment;node-type=cmd;type=child;name=edit;node-type=cmd;type=child;name=export;node-type=cmd;type=child;name=find;node-type=cmd;type=child;name=get;node-type=cmd;type=child;name=member;node-type=dir;type=child;name=print;node-type=cmd;type=child;name=remove;node-type=cmd;type=child;name=reset;node-type=cmd;type=child;name=set;node-type=cmd;type=child",
expected: ConsoleItem{
Self: Item{
Name: "list",
NodeType: NodeTypeDir,
Type: TypeSelf,
},
Commands: []string{
"add",
"comment",
"edit",
"export",
"find",
"get",
"print",
"remove",
"reset",
"set",
},
Subcommands: []string{"member"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
item, err := Parse(tc.input, DefaultSplitStrategy)
if !assert.Equal(t, !tc.expectedError, err == nil) || tc.expectedError {
return
}
assert.Equal(t, tc.expected, item)
})
}
}
38 changes: 38 additions & 0 deletions client/console-inspected/split_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package consoleinspected

import "strings"

var DefaultSplitStrategy = splitStrategyFunc(orderedSplit)

type splitStrategyFunc func(string) ([]string, error)

func (f splitStrategyFunc) Split(in string) ([]string, error) {
return f(in)
}

// orderedSplit splits items definition using order of fields.
//
// Each 'name=' key starts a new item definition.
func orderedSplit(in string) ([]string, error) {
result := []string{}

buf := strings.Builder{}
for _, v := range strings.Split(in, ";") {
if strings.TrimSpace(v) == "" {
continue
}
if strings.HasPrefix(v, "name=") {
if buf.Len() > 0 {
result = append(result, buf.String())
}
buf.Reset()
}
buf.WriteString(v)
buf.WriteString(";")
}
if buf.Len() > 0 {
result = append(result, buf.String())
}

return result, nil
}
58 changes: 58 additions & 0 deletions client/console-inspected/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package consoleinspected

const (
// NodeTypeDir represents console menu level.
NodeTypeDir NodeType = "dir"

// NodeTypeCommand represents console command that can be called.
NodeTypeCommand NodeType = "cmd"

// NodeTypeArg represents console item that is argument to a command.
NodeTypeArg NodeType = "arg"

// TypeSelf is console item type for currently inspected item.
TypeSelf Type = "self"

// TypeChild is console item type of all items within inspected container.
TypeChild Type = "child"
)

type (
// NodeType is dedicated type that holds values of "node-type" field of console item.
NodeType string

// Type is dedicated type that holds values of "type" field of console item.
Type string

// Item represents inspected console items.
Item struct {
NodeType NodeType `mikrotik:"node-type"`
Type Type `mikrotik:"type"`
Name string `mikrotik:"name"`
}

// ConsoleItem represents inspected console item with extracted commands, arguments, etc.
ConsoleItem struct {
// Self holds information about current console item.
Self Item

// Commands holds a list of commands available for this menu level.
// Valid only for ConsoleItem of type NodeTypeDir.
Commands []string

// Subcommands holds a list of commands for the nested menu level.
// Valid only for ConsoleItem of type NodeTypeDir.
Subcommands []string

// Arguments holds a list of argument items for a command.
// Valid only for ConsoleItem of type NodeItemCommand.
Arguments []Item
}
)

type (
ItemsDefinitionSplitStrategy interface {
// Split splits set of items definition represented by a single string into chunks of separate item definitions.
Split(string) ([]string, error)
}
)
42 changes: 42 additions & 0 deletions client/console_inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package client

import (
"strings"

consoleinspected "github.com/ddelnano/terraform-provider-mikrotik/client/console-inspected"
)

func (c Mikrotik) InspectConsoleCommand(command string) (consoleinspected.ConsoleItem, error) {
client, err := c.getMikrotikClient()
if err != nil {
return consoleinspected.ConsoleItem{}, err
}
normalizedCommand := strings.ReplaceAll(command[1:], "/", ",")
cmd := []string{"/console/inspect", "as-value", "=path=" + normalizedCommand, "=request=child"}
reply, err := client.RunArgs(cmd)
if err != nil {
return consoleinspected.ConsoleItem{}, err
}
var items []consoleinspected.Item
var result consoleinspected.ConsoleItem
if err := Unmarshal(*reply, &items); err != nil {
return consoleinspected.ConsoleItem{}, err
}

for _, v := range items {
if v.Type == consoleinspected.TypeSelf {
result.Self = v
continue
}
switch v.NodeType {
case consoleinspected.NodeTypeArg:
result.Arguments = append(result.Arguments, v)
case consoleinspected.NodeTypeCommand:
result.Commands = append(result.Commands, v.Name)
case consoleinspected.NodeTypeDir:
result.Subcommands = append(result.Subcommands, v.Name)
}
}

return result, nil
}
100 changes: 100 additions & 0 deletions cmd/mikrotik-codegen/internal/codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ where

`commandBase` - base path to craft commands for CRUD operations.

It is also possible to pre-fill list of fields using either `-inspect-definition-file` argument
```sh
$ go run ./cmd/mikrotik-codegen mikrotik -name BridgeVlan -commandBase "/interface/bridge/vlan" -inspect-definition-file ./inspect_vlan.txt
```

or `-query-definition` flag (requires valid credentials in evironment)
```sh
$ go run ./cmd/mikrotik-codegen mikrotik -name BridgeVlan -commandBase "/interface/bridge/vlan" -query-definition
```
For details, see [Experimental](#experimental) section.

## Terraform resource
Just add a `codegen` tag key to struct fields:
```go
Expand Down Expand Up @@ -47,3 +58,92 @@ $ go run ./cmd/mikrotik-codegen terraform -src client/resource.go -struct Mikrot
|computed|Mark field as `computed` in resource schema|
|elemType|Explicitly set element type for `List` or `Set` attributes. Usage `elemType=int`|
|omit|Skip this field from code generation process|


## Experimental

This section contains documentation for experimental and non-stable features.

### Generate Mikrotik resource using /console/inspect definition

Modern RouterOS versions (>7.x) provide new `/console/inspect` command to query hierarchy or syntax of particular command.

For example, `/console/inspect path=interface,list request=child` prints `parent-child` relationship of the command:
```
Columns: TYPE, NAME, NODE-TYPE
TYPE NAME NODE-TYPE
self list dir
child add cmd
child comment cmd
child edit cmd
child export cmd
child find cmd
child get cmd
child member dir
child print cmd
child remove cmd
child reset cmd
child set cmd
```

while `/console/inspect path=interface,list request=syntax` gives another set of attributes:
```
Columns: TYPE, SYMBOL, SYMBOL-TYPE, NESTED, NONORM, TEXT
TYPE SYMBOL SYMBOL-TYPE NESTED NONORM TEXT
syntax collection 0 yes
syntax .. explanation 1 no go up to interface
syntax add explanation 1 no Create a new item
syntax comment explanation 1 no Set comment for items
syntax edit explanation 1 no
syntax export explanation 1 no Print or save an export script that can be used to restore configuration
syntax find explanation 1 no Find items by value
syntax get explanation 1 no Gets value of item's property
syntax member explanation 1 no
syntax print explanation 1 no Print values of item properties
syntax remove explanation 1 no Remove item
syntax reset explanation 1 no
syntax set explanation 1 no Change item properties
```

Using that information, it is possible to query (even recursively) information about all menu items and sub-commands, starting from the root `/` command.

:Warning: Since this feature is recent, trying to call it with our client package sometimes results in `terminal crush`.

#### Using definition in file

For this case, one needs:
1. Machine-readable data about available fields
2. Pass this data as `-inspect-definition-file` argument.

To get resource definition, run the following command on remote system:
```
$ :put [/console/inspect path=interface,list,add request=child as-value]
```

which produces:
```
name=add;node-type=cmd;type=self;name=comment;node-type=arg;type=child;name=copy-from;node-type=arg;type=child;name=exclude;node-type=arg;type=child;name=include;node-type=arg;type=child;name=name;node-type=arg;type=child
```

If you have `ssh` access to the Mikrotik, the following command will produce the same string:
```shell
$ ssh -o Port=XXXX -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null admin@board ":put [/console inspect as-value request=child path=interface,list,add]" > inspect_definition.txt
```

After getting the definition file, just generate Mikrotik resource as usual with extra flag:
```sh
$ go run ./cmd/mikrotik-codegen mikrotik -name InterfaceList -commandBase "/interface/list" -inspect-definition-file ./inspect_definition.txt
```
and all fields for the struct will be created.


Note, that we used `interface,list,add` as argument to `path`. The terminal equivalent would be `/interface/list/add` (not sure why it works that way, you can check [forum topic](https://forum.mikrotik.com/viewtopic.php?t=199139#p1024410))

The reason we used `add` command and not `/interface/list` menu itself, is that we need only args (fields) of `add` command - not information about possible commands for `/interface/list`
#### Query resource definition automatically

To use this method, current environment must contain valid credentials.

```shell
$ go run cmd/mikrotik-codegen/main.go mikrotik -commandBase /ip/dhcp-server -name DhcpServer -query-definition
```
Loading

0 comments on commit 326ca07

Please sign in to comment.