1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
The following examples are in the form as entered in the GraphQL
'playground', which is found at:
https://{{ host_address }}/graphql
Example using GraphQL mutations to configure a DHCP server:
All examples assume that the http-api is running:
'set service https api'
One can configure an address on an interface, and configure the DHCP server
to run with that address as default router by requesting these 'mutations'
in the GraphQL playground:
mutation {
CreateInterfaceEthernet (data: {interface: "eth1",
address: "192.168.0.1/24",
description: "BOB"}) {
success
errors
data {
address
}
}
}
mutation {
CreateDhcpServer(data: {sharedNetworkName: "BOB",
subnet: "192.168.0.0/24",
defaultRouter: "192.168.0.1",
nameServer: "192.168.0.1",
domainName: "vyos.net",
lease: 86400,
range: 0,
start: "192.168.0.9",
stop: "192.168.0.254",
dnsForwardingAllowFrom: "192.168.0.0/24",
dnsForwardingCacheSize: 0,
dnsForwardingListenAddress: "192.168.0.1"}) {
success
errors
data {
defaultRouter
}
}
}
To save the configuration, use the following mutation:
mutation {
SaveConfigFile(data: {fileName: "/config/config.boot"}) {
success
errors
data {
fileName
}
}
}
N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to
save to /config/config.boot; to save to an alternative path, specify
fileName.
Similarly, using an analogous 'endpoint' (meaning the form of the request
and resolver; the actual enpoint for all GraphQL requests is
https://hostname/graphql), one can load an arbitrary config file from a
path.
mutation {
LoadConfigFile(data: {fileName: "/home/vyos/config.boot"}) {
success
errors
data {
fileName
}
}
}
Op-mode 'show' commands may be requested by path, e.g.:
query {
Show (data: {path: ["interfaces", "ethernet", "detail"]}) {
success
errors
data {
result
}
}
}
N.B. to see the output the 'data' field 'result' must be present in the
request.
Mutations to manipulate firewall address groups:
mutation {
CreateFirewallAddressGroup (data: {name: "ADDR-GRP", address: "10.0.0.1"}) {
success
errors
}
}
mutation {
UpdateFirewallAddressGroupMembers (data: {name: "ADDR-GRP",
address: ["10.0.0.1-10.0.0.8", "192.168.0.1"]}) {
success
errors
}
}
mutation {
RemoveFirewallAddressGroupMembers (data: {name: "ADDR-GRP",
address: "192.168.0.1"}) {
success
errors
}
}
N.B. The schema for the above specify that 'address' be of the form 'list of
strings' (SDL type [String!]! for UpdateFirewallAddressGroupMembers, where
the ! indicates that the input is required; SDL type [String] in
CreateFirewallAddressGroup, since a group may be created without any
addresses). However, notice that a single string may be passed without being
a member of a list, in which case the specification allows for 'input
coercion':
http://spec.graphql.org/October2021/#sec-Scalars.Input-Coercion
Similarly, IPv6 versions of the above:
CreateFirewallAddressIpv6Group
UpdateFirewallAddressIpv6GroupMembers
RemoveFirewallAddressIpv6GroupMembers
Instead of using the GraphQL playground, an equivalent curl command to the
first example above would be:
curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}'
Note that the 'mutation' term is prefaced by 'query' in the curl command.
Curl equivalents may be read from within the GraphQL playground at the 'copy
curl' button.
What's here:
services
├── api
│ └── graphql
│ ├── bindings.py
│ ├── graphql
│ │ ├── directives.py
│ │ ├── __init__.py
│ │ ├── mutations.py
│ │ └── schema
│ │ ├── config_file.graphql
│ │ ├── dhcp_server.graphql
│ │ ├── firewall_group.graphql
│ │ ├── interface_ethernet.graphql
│ │ ├── schema.graphql
│ │ ├── show_config.graphql
│ │ └── show.graphql
│ ├── README.graphql
│ ├── recipes
│ │ ├── __init__.py
│ │ ├── remove_firewall_address_group_members.py
│ │ ├── session.py
│ │ └── templates
│ │ ├── create_dhcp_server.tmpl
│ │ ├── create_firewall_address_group.tmpl
│ │ ├── create_interface_ethernet.tmpl
│ │ ├── remove_firewall_address_group_members.tmpl
│ │ └── update_firewall_address_group_members.tmpl
│ └── state.py
├── vyos-configd
├── vyos-hostsd
└── vyos-http-api-server
The GraphQL library that we are using, Ariadne, advertises itself as a
'schema-first' implementation: define the schema; define resolvers
(handlers) for declared Query and Mutation types (Subscription types are not
currently used).
In the current approach to a high-level API, we consider the
Jinja2-templated collection of configuration mode 'set'/'delete' commands as
the Ur-data; the GraphQL schema is produced from those files, located in
'api/graphql/recipes/templates'.
Resolvers for the schema Mutation fields are dynamically generated using a
'directive' added to the respective schema field. The directive,
'@configure', is handled by the class 'ConfigureDirective' in
'api/graphql/graphql/directives.py', which calls the
'make_configure_resolver' function in 'api/graphql/graphql/mutations.py';
the produced resolver calls the appropriate wrapper in
'api/graphql/recipes', with base class doing the (overridable) configuration
steps of calling all defined 'set'/'delete' commands.
Integrating the above with vyos-http-api-server is 4 lines of code.
What needs to be done:
• automate generation of schema and wrappers from templated configuration
commands
• investigate whether the subclassing provided by the named wrappers in
'api/graphql/recipes' is sufficient for use cases which need to modify data
• encapsulate the manipulation of 'canonical names' which transforms the
prefixed camel-case schema names to various snake-case file/function names
• consider mechanism for migration of templates: offline vs. on-the-fly
• define the naming convention for those schema fields that refer to
configuration mode parameters: e.g. how much of the path is needed as prefix
to uniquely define the term
|