]>
Commit | Line | Data |
---|---|---|
777ece09 | 1 | /* |
f9ac0f03 | 2 | * Copyright (c) 2010, 2011, 2012, 2013, 2014, 2017 Nicira, Inc. |
68da36fe | 3 | * Copyright (c) 2016 Red Hat, Inc. |
777ece09 JG |
4 | * |
5 | * Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | * you may not use this file except in compliance with the License. | |
7 | * You may obtain a copy of the License at: | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
6fcfff1b | 11 | * Unless required by applicable law or agreed to in writing, software |
777ece09 JG |
12 | * distributed under the License is distributed on an "AS IS" BASIS, |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | ||
18 | #include <config.h> | |
2b9d6589 BP |
19 | |
20 | #include "netdev-vport.h" | |
21 | ||
777ece09 JG |
22 | #include <errno.h> |
23 | #include <fcntl.h> | |
ea83a2fc | 24 | #include <sys/socket.h> |
2b9d6589 | 25 | #include <net/if.h> |
2456f331 | 26 | #include <netinet/in.h> |
370e373b | 27 | #include <netinet/ip6.h> |
777ece09 JG |
28 | #include <sys/ioctl.h> |
29 | ||
b9298d3f | 30 | #include "byte-order.h" |
5059eff3 JP |
31 | #include "daemon.h" |
32 | #include "dirs.h" | |
0a740f48 | 33 | #include "dpif.h" |
aca40d4f TLSC |
34 | #include "netdev.h" |
35 | #include "netdev-native-tnl.h" | |
2b9d6589 | 36 | #include "netdev-provider.h" |
6b241d64 | 37 | #include "netdev-vport-private.h" |
9fff138e | 38 | #include "openvswitch/dynamic-string.h" |
d9b4ebc5 | 39 | #include "ovs-router.h" |
2b9d6589 | 40 | #include "packets.h" |
41ca1e0a | 41 | #include "poll-loop.h" |
a132aa96 | 42 | #include "route-table.h" |
aca40d4f | 43 | #include "smap.h" |
777ece09 | 44 | #include "socket-util.h" |
a36de779 PS |
45 | #include "unaligned.h" |
46 | #include "unixctl.h" | |
aca40d4f | 47 | #include "openvswitch/vlog.h" |
777ece09 | 48 | |
d98e6007 | 49 | VLOG_DEFINE_THIS_MODULE(netdev_vport); |
5136ce49 | 50 | |
c1fc1411 | 51 | #define GENEVE_DST_PORT 6081 |
4f2abb7b | 52 | #define VXLAN_DST_PORT 4789 |
a6ae068b | 53 | #define LISP_DST_PORT 4341 |
4237026e | 54 | #define STT_DST_PORT 7471 |
a6ae068b | 55 | |
f431bf7d EJ |
56 | #define DEFAULT_TTL 64 |
57 | ||
41ca1e0a AW |
58 | /* Last read of the route-table's change number. */ |
59 | static uint64_t rt_change_seqno; | |
60 | ||
86383816 | 61 | static int get_patch_config(const struct netdev *netdev, struct smap *args); |
b5d57fc8 | 62 | static int get_tunnel_config(const struct netdev *, struct smap *args); |
41ca1e0a | 63 | static bool tunnel_check_status_change__(struct netdev_vport *); |
2b9d6589 | 64 | |
6b241d64 PS |
65 | struct vport_class { |
66 | const char *dpif_port; | |
67 | struct netdev_class netdev_class; | |
68 | }; | |
777ece09 | 69 | |
41ca1e0a AW |
70 | bool |
71 | netdev_vport_is_vport_class(const struct netdev_class *class) | |
72 | { | |
73 | return is_vport_class(class); | |
74 | } | |
75 | ||
2b9d6589 BP |
76 | static const struct vport_class * |
77 | vport_class_cast(const struct netdev_class *class) | |
78 | { | |
cb22974d | 79 | ovs_assert(is_vport_class(class)); |
2b9d6589 BP |
80 | return CONTAINER_OF(class, struct vport_class, netdev_class); |
81 | } | |
82 | ||
f431bf7d | 83 | static const struct netdev_tunnel_config * |
b5d57fc8 | 84 | get_netdev_tunnel_config(const struct netdev *netdev) |
f431bf7d | 85 | { |
b5d57fc8 | 86 | return &netdev_vport_cast(netdev)->tnl_cfg; |
f431bf7d EJ |
87 | } |
88 | ||
0a740f48 EJ |
89 | bool |
90 | netdev_vport_is_patch(const struct netdev *netdev) | |
91 | { | |
b5d57fc8 | 92 | const struct netdev_class *class = netdev_get_class(netdev); |
f18a39b7 | 93 | |
c060c4cf | 94 | return class->get_config == get_patch_config; |
0a740f48 EJ |
95 | } |
96 | ||
a6363cfd LJ |
97 | bool |
98 | netdev_vport_is_layer3(const struct netdev *dev) | |
99 | { | |
beb75a40 JS |
100 | if (is_vport_class(netdev_get_class(dev))) { |
101 | struct netdev_vport *vport = netdev_vport_cast(dev); | |
102 | ||
103 | return vport->tnl_cfg.is_layer3; | |
104 | } | |
a6363cfd | 105 | |
beb75a40 | 106 | return false; |
a6363cfd LJ |
107 | } |
108 | ||
56b11f0b | 109 | static bool |
b5d57fc8 | 110 | netdev_vport_needs_dst_port(const struct netdev *dev) |
56b11f0b | 111 | { |
b5d57fc8 BP |
112 | const struct netdev_class *class = netdev_get_class(dev); |
113 | const char *type = netdev_get_type(dev); | |
56b11f0b | 114 | |
a6ae068b | 115 | return (class->get_config == get_tunnel_config && |
c1fc1411 | 116 | (!strcmp("geneve", type) || !strcmp("vxlan", type) || |
4237026e | 117 | !strcmp("lisp", type) || !strcmp("stt", type)) ); |
56b11f0b KM |
118 | } |
119 | ||
94a53842 AW |
120 | const char * |
121 | netdev_vport_class_get_dpif_port(const struct netdev_class *class) | |
122 | { | |
123 | return is_vport_class(class) ? vport_class_cast(class)->dpif_port : NULL; | |
124 | } | |
125 | ||
de281153 | 126 | const char * |
3aa30359 BP |
127 | netdev_vport_get_dpif_port(const struct netdev *netdev, |
128 | char namebuf[], size_t bufsize) | |
de281153 | 129 | { |
a5d4fadd JG |
130 | const struct netdev_class *class = netdev_get_class(netdev); |
131 | const char *dpif_port = netdev_vport_class_get_dpif_port(class); | |
132 | ||
133 | if (!dpif_port) { | |
134 | return netdev_get_name(netdev); | |
135 | } | |
136 | ||
b5d57fc8 BP |
137 | if (netdev_vport_needs_dst_port(netdev)) { |
138 | const struct netdev_vport *vport = netdev_vport_cast(netdev); | |
56b11f0b KM |
139 | |
140 | /* | |
a5d4fadd JG |
141 | * Note: IFNAMSIZ is 16 bytes long. Implementations should choose |
142 | * a dpif port name that is short enough to fit including any | |
143 | * port numbers but assert just in case. | |
56b11f0b | 144 | */ |
3aa30359 | 145 | BUILD_ASSERT(NETDEV_VPORT_NAME_BUFSIZE >= IFNAMSIZ); |
a5d4fadd JG |
146 | ovs_assert(strlen(dpif_port) + 6 < IFNAMSIZ); |
147 | snprintf(namebuf, bufsize, "%s_%d", dpif_port, | |
56b11f0b | 148 | ntohs(vport->tnl_cfg.dst_port)); |
3aa30359 | 149 | return namebuf; |
56b11f0b | 150 | } else { |
a5d4fadd | 151 | return dpif_port; |
56b11f0b | 152 | } |
2b9d6589 | 153 | } |
777ece09 | 154 | |
41ca1e0a AW |
155 | /* Whenever the route-table change number is incremented, |
156 | * netdev_vport_route_changed() should be called to update | |
157 | * the corresponding tunnel interface status. */ | |
158 | static void | |
159 | netdev_vport_route_changed(void) | |
160 | { | |
161 | struct netdev **vports; | |
162 | size_t i, n_vports; | |
163 | ||
164 | vports = netdev_get_vports(&n_vports); | |
165 | for (i = 0; i < n_vports; i++) { | |
166 | struct netdev *netdev_ = vports[i]; | |
167 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); | |
168 | ||
169 | ovs_mutex_lock(&netdev->mutex); | |
170 | /* Finds all tunnel vports. */ | |
3ae91c01 | 171 | if (ipv6_addr_is_set(&netdev->tnl_cfg.ipv6_dst)) { |
41ca1e0a AW |
172 | if (tunnel_check_status_change__(netdev)) { |
173 | netdev_change_seq_changed(netdev_); | |
174 | } | |
175 | } | |
41ca1e0a | 176 | ovs_mutex_unlock(&netdev->mutex); |
b2f771ef BP |
177 | |
178 | netdev_close(netdev_); | |
41ca1e0a AW |
179 | } |
180 | ||
181 | free(vports); | |
182 | } | |
183 | ||
9dc63482 BP |
184 | static struct netdev * |
185 | netdev_vport_alloc(void) | |
186 | { | |
187 | struct netdev_vport *netdev = xzalloc(sizeof *netdev); | |
188 | return &netdev->up; | |
189 | } | |
190 | ||
6b241d64 | 191 | int |
9dc63482 | 192 | netdev_vport_construct(struct netdev *netdev_) |
2b9d6589 | 193 | { |
a36de779 PS |
194 | struct netdev_vport *dev = netdev_vport_cast(netdev_); |
195 | const char *type = netdev_get_type(netdev_); | |
6d9e6eb4 | 196 | |
a36de779 | 197 | ovs_mutex_init(&dev->mutex); |
74ff3298 | 198 | eth_addr_random(&dev->etheraddr); |
a36de779 PS |
199 | |
200 | /* Add a default destination port for tunnel ports if none specified. */ | |
201 | if (!strcmp(type, "geneve")) { | |
202 | dev->tnl_cfg.dst_port = htons(GENEVE_DST_PORT); | |
203 | } else if (!strcmp(type, "vxlan")) { | |
204 | dev->tnl_cfg.dst_port = htons(VXLAN_DST_PORT); | |
205 | } else if (!strcmp(type, "lisp")) { | |
206 | dev->tnl_cfg.dst_port = htons(LISP_DST_PORT); | |
4237026e PS |
207 | } else if (!strcmp(type, "stt")) { |
208 | dev->tnl_cfg.dst_port = htons(STT_DST_PORT); | |
a36de779 | 209 | } |
6d9e6eb4 | 210 | |
0890056e PS |
211 | dev->tnl_cfg.dont_fragment = true; |
212 | dev->tnl_cfg.ttl = DEFAULT_TTL; | |
de5cdb90 | 213 | return 0; |
777ece09 JG |
214 | } |
215 | ||
2b9d6589 | 216 | static void |
9dc63482 | 217 | netdev_vport_destruct(struct netdev *netdev_) |
2b9d6589 | 218 | { |
b5d57fc8 | 219 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); |
2b9d6589 | 220 | |
b5d57fc8 | 221 | free(netdev->peer); |
86383816 | 222 | ovs_mutex_destroy(&netdev->mutex); |
9dc63482 BP |
223 | } |
224 | ||
225 | static void | |
226 | netdev_vport_dealloc(struct netdev *netdev_) | |
227 | { | |
228 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); | |
2b9d6589 BP |
229 | free(netdev); |
230 | } | |
231 | ||
2b9d6589 | 232 | static int |
74ff3298 | 233 | netdev_vport_set_etheraddr(struct netdev *netdev_, const struct eth_addr mac) |
777ece09 | 234 | { |
b5d57fc8 | 235 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); |
86383816 BP |
236 | |
237 | ovs_mutex_lock(&netdev->mutex); | |
74ff3298 | 238 | netdev->etheraddr = mac; |
86383816 | 239 | ovs_mutex_unlock(&netdev->mutex); |
3e912ffc | 240 | netdev_change_seq_changed(netdev_); |
86383816 | 241 | |
35b769cb | 242 | return 0; |
777ece09 JG |
243 | } |
244 | ||
2b9d6589 | 245 | static int |
74ff3298 | 246 | netdev_vport_get_etheraddr(const struct netdev *netdev_, struct eth_addr *mac) |
777ece09 | 247 | { |
86383816 BP |
248 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); |
249 | ||
250 | ovs_mutex_lock(&netdev->mutex); | |
74ff3298 | 251 | *mac = netdev->etheraddr; |
86383816 BP |
252 | ovs_mutex_unlock(&netdev->mutex); |
253 | ||
35b769cb | 254 | return 0; |
777ece09 JG |
255 | } |
256 | ||
41ca1e0a AW |
257 | /* Checks if the tunnel status has changed and returns a boolean. |
258 | * Updates the tunnel status if it has changed. */ | |
259 | static bool | |
260 | tunnel_check_status_change__(struct netdev_vport *netdev) | |
261 | OVS_REQUIRES(netdev->mutex) | |
ea763e0e | 262 | { |
3dea0874 | 263 | char iface[IFNAMSIZ]; |
41ca1e0a | 264 | bool status = false; |
3ae91c01 JB |
265 | struct in6_addr *route; |
266 | struct in6_addr gw; | |
ed52ca57 | 267 | uint32_t mark; |
ea763e0e | 268 | |
41ca1e0a | 269 | iface[0] = '\0'; |
3ae91c01 | 270 | route = &netdev->tnl_cfg.ipv6_dst; |
ed52ca57 PS |
271 | mark = netdev->tnl_cfg.egress_pkt_mark; |
272 | if (ovs_router_lookup(mark, route, iface, NULL, &gw)) { | |
a404826e AE |
273 | struct netdev *egress_netdev; |
274 | ||
6c607a64 | 275 | if (!netdev_open(iface, NULL, &egress_netdev)) { |
41ca1e0a | 276 | status = netdev_get_carrier(egress_netdev); |
a404826e AE |
277 | netdev_close(egress_netdev); |
278 | } | |
ea763e0e EJ |
279 | } |
280 | ||
41ca1e0a AW |
281 | if (strcmp(netdev->egress_iface, iface) |
282 | || netdev->carrier_status != status) { | |
f9ac0f03 | 283 | ovs_strlcpy_arrays(netdev->egress_iface, iface); |
41ca1e0a AW |
284 | netdev->carrier_status = status; |
285 | ||
286 | return true; | |
287 | } | |
288 | ||
289 | return false; | |
290 | } | |
291 | ||
292 | static int | |
293 | tunnel_get_status(const struct netdev *netdev_, struct smap *smap) | |
294 | { | |
295 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); | |
296 | ||
297 | if (netdev->egress_iface[0]) { | |
298 | smap_add(smap, "tunnel_egress_iface", netdev->egress_iface); | |
299 | ||
300 | smap_add(smap, "tunnel_egress_iface_carrier", | |
301 | netdev->carrier_status ? "up" : "down"); | |
302 | } | |
303 | ||
ea763e0e EJ |
304 | return 0; |
305 | } | |
306 | ||
2b9d6589 | 307 | static int |
b5d57fc8 BP |
308 | netdev_vport_update_flags(struct netdev *netdev OVS_UNUSED, |
309 | enum netdev_flags off, | |
310 | enum netdev_flags on OVS_UNUSED, | |
311 | enum netdev_flags *old_flagsp) | |
777ece09 JG |
312 | { |
313 | if (off & (NETDEV_UP | NETDEV_PROMISC)) { | |
314 | return EOPNOTSUPP; | |
315 | } | |
316 | ||
317 | *old_flagsp = NETDEV_UP | NETDEV_PROMISC; | |
318 | return 0; | |
319 | } | |
320 | ||
ea83a2fc | 321 | static void |
1c33f0c3 | 322 | netdev_vport_run(const struct netdev_class *netdev_class OVS_UNUSED) |
ea83a2fc | 323 | { |
41ca1e0a AW |
324 | uint64_t seq; |
325 | ||
a132aa96 | 326 | route_table_run(); |
41ca1e0a AW |
327 | seq = route_table_get_change_seq(); |
328 | if (rt_change_seqno != seq) { | |
329 | rt_change_seqno = seq; | |
330 | netdev_vport_route_changed(); | |
331 | } | |
ea83a2fc EJ |
332 | } |
333 | ||
334 | static void | |
1c33f0c3 | 335 | netdev_vport_wait(const struct netdev_class *netdev_class OVS_UNUSED) |
ea83a2fc | 336 | { |
41ca1e0a AW |
337 | uint64_t seq; |
338 | ||
a132aa96 | 339 | route_table_wait(); |
41ca1e0a AW |
340 | seq = route_table_get_change_seq(); |
341 | if (rt_change_seqno != seq) { | |
342 | poll_immediate_wake(); | |
343 | } | |
ea83a2fc EJ |
344 | } |
345 | \f | |
0a740f48 | 346 | /* Code specific to tunnel types. */ |
2b9d6589 | 347 | |
f431bf7d EJ |
348 | static ovs_be64 |
349 | parse_key(const struct smap *args, const char *name, | |
350 | bool *present, bool *flow) | |
c19e6535 BP |
351 | { |
352 | const char *s; | |
353 | ||
f431bf7d EJ |
354 | *present = false; |
355 | *flow = false; | |
356 | ||
79f1cbe9 | 357 | s = smap_get(args, name); |
c19e6535 | 358 | if (!s) { |
79f1cbe9 | 359 | s = smap_get(args, "key"); |
c19e6535 | 360 | if (!s) { |
f431bf7d | 361 | return 0; |
c19e6535 BP |
362 | } |
363 | } | |
364 | ||
f431bf7d EJ |
365 | *present = true; |
366 | ||
c19e6535 | 367 | if (!strcmp(s, "flow")) { |
f431bf7d EJ |
368 | *flow = true; |
369 | return 0; | |
c19e6535 | 370 | } else { |
f431bf7d | 371 | return htonll(strtoull(s, NULL, 0)); |
c19e6535 BP |
372 | } |
373 | } | |
374 | ||
3ae91c01 JB |
375 | static int |
376 | parse_tunnel_ip(const char *value, bool accept_mcast, bool *flow, | |
377 | struct in6_addr *ipv6, uint16_t *protocol) | |
378 | { | |
379 | if (!strcmp(value, "flow")) { | |
380 | *flow = true; | |
381 | *protocol = 0; | |
382 | return 0; | |
383 | } | |
384 | if (addr_is_ipv6(value)) { | |
385 | if (lookup_ipv6(value, ipv6)) { | |
386 | return ENOENT; | |
387 | } | |
388 | if (!accept_mcast && ipv6_addr_is_multicast(ipv6)) { | |
389 | return EINVAL; | |
390 | } | |
391 | *protocol = ETH_TYPE_IPV6; | |
392 | } else { | |
393 | struct in_addr ip; | |
394 | if (lookup_ip(value, &ip)) { | |
395 | return ENOENT; | |
396 | } | |
397 | if (!accept_mcast && ip_is_multicast(ip.s_addr)) { | |
398 | return EINVAL; | |
399 | } | |
400 | in6_addr_set_mapped_ipv4(ipv6, ip.s_addr); | |
401 | *protocol = ETH_TYPE_IP; | |
402 | } | |
403 | return 0; | |
404 | } | |
405 | ||
2b9d6589 | 406 | static int |
9fff138e | 407 | set_tunnel_config(struct netdev *dev_, const struct smap *args, char **errp) |
2b9d6589 | 408 | { |
b5d57fc8 BP |
409 | struct netdev_vport *dev = netdev_vport_cast(dev_); |
410 | const char *name = netdev_get_name(dev_); | |
411 | const char *type = netdev_get_type(dev_); | |
9fff138e | 412 | struct ds errors = DS_EMPTY_INITIALIZER; |
63171f04 | 413 | bool needs_dst_port, has_csum, optional_layer3; |
3ae91c01 | 414 | uint16_t dst_proto = 0, src_proto = 0; |
f431bf7d | 415 | struct netdev_tunnel_config tnl_cfg; |
79f1cbe9 | 416 | struct smap_node *node; |
439f39cb | 417 | bool is_layer3 = false; |
9fff138e | 418 | int err; |
f431bf7d | 419 | |
4752cc0c | 420 | has_csum = strstr(type, "gre") || strstr(type, "geneve") || |
4237026e | 421 | strstr(type, "stt") || strstr(type, "vxlan"); |
63171f04 | 422 | optional_layer3 = !strcmp(type, "gre"); |
f431bf7d | 423 | memset(&tnl_cfg, 0, sizeof tnl_cfg); |
2b9d6589 | 424 | |
a36de779 PS |
425 | /* Add a default destination port for tunnel ports if none specified. */ |
426 | if (!strcmp(type, "geneve")) { | |
427 | tnl_cfg.dst_port = htons(GENEVE_DST_PORT); | |
428 | } | |
429 | ||
430 | if (!strcmp(type, "vxlan")) { | |
431 | tnl_cfg.dst_port = htons(VXLAN_DST_PORT); | |
432 | } | |
433 | ||
434 | if (!strcmp(type, "lisp")) { | |
435 | tnl_cfg.dst_port = htons(LISP_DST_PORT); | |
63171f04 | 436 | tnl_cfg.is_layer3 = true; |
a36de779 PS |
437 | } |
438 | ||
4237026e PS |
439 | if (!strcmp(type, "stt")) { |
440 | tnl_cfg.dst_port = htons(STT_DST_PORT); | |
441 | } | |
442 | ||
a6ae068b | 443 | needs_dst_port = netdev_vport_needs_dst_port(dev_); |
f431bf7d | 444 | tnl_cfg.dont_fragment = true; |
e16a28b5 | 445 | |
79f1cbe9 EJ |
446 | SMAP_FOR_EACH (node, args) { |
447 | if (!strcmp(node->key, "remote_ip")) { | |
3ae91c01 JB |
448 | err = parse_tunnel_ip(node->value, false, &tnl_cfg.ip_dst_flow, |
449 | &tnl_cfg.ipv6_dst, &dst_proto); | |
450 | switch (err) { | |
451 | case ENOENT: | |
9fff138e | 452 | ds_put_format(&errors, "%s: bad %s 'remote_ip'\n", name, type); |
3ae91c01 JB |
453 | break; |
454 | case EINVAL: | |
9fff138e DDP |
455 | ds_put_format(&errors, |
456 | "%s: multicast remote_ip=%s not allowed\n", | |
457 | name, node->value); | |
458 | goto out; | |
2b9d6589 | 459 | } |
79f1cbe9 | 460 | } else if (!strcmp(node->key, "local_ip")) { |
3ae91c01 JB |
461 | err = parse_tunnel_ip(node->value, true, &tnl_cfg.ip_src_flow, |
462 | &tnl_cfg.ipv6_src, &src_proto); | |
463 | switch (err) { | |
464 | case ENOENT: | |
9fff138e | 465 | ds_put_format(&errors, "%s: bad %s 'local_ip'\n", name, type); |
3ae91c01 | 466 | break; |
2b9d6589 | 467 | } |
79f1cbe9 EJ |
468 | } else if (!strcmp(node->key, "tos")) { |
469 | if (!strcmp(node->value, "inherit")) { | |
f431bf7d | 470 | tnl_cfg.tos_inherit = true; |
2b9d6589 | 471 | } else { |
3fca7064 PS |
472 | char *endptr; |
473 | int tos; | |
79f1cbe9 | 474 | tos = strtol(node->value, &endptr, 0); |
91aff446 | 475 | if (*endptr == '\0' && tos == (tos & IP_DSCP_MASK)) { |
f431bf7d | 476 | tnl_cfg.tos = tos; |
91aff446 | 477 | } else { |
9fff138e DDP |
478 | ds_put_format(&errors, "%s: invalid TOS %s\n", name, |
479 | node->value); | |
3fca7064 | 480 | } |
2b9d6589 | 481 | } |
79f1cbe9 EJ |
482 | } else if (!strcmp(node->key, "ttl")) { |
483 | if (!strcmp(node->value, "inherit")) { | |
f431bf7d | 484 | tnl_cfg.ttl_inherit = true; |
2b9d6589 | 485 | } else { |
f431bf7d | 486 | tnl_cfg.ttl = atoi(node->value); |
2b9d6589 | 487 | } |
79f827fa | 488 | } else if (!strcmp(node->key, "dst_port") && needs_dst_port) { |
f431bf7d | 489 | tnl_cfg.dst_port = htons(atoi(node->value)); |
f431bf7d | 490 | } else if (!strcmp(node->key, "csum") && has_csum) { |
79f1cbe9 | 491 | if (!strcmp(node->value, "true")) { |
f431bf7d | 492 | tnl_cfg.csum = true; |
2b9d6589 | 493 | } |
79f1cbe9 EJ |
494 | } else if (!strcmp(node->key, "df_default")) { |
495 | if (!strcmp(node->value, "false")) { | |
f431bf7d | 496 | tnl_cfg.dont_fragment = false; |
66409d1b | 497 | } |
79f1cbe9 EJ |
498 | } else if (!strcmp(node->key, "key") || |
499 | !strcmp(node->key, "in_key") || | |
500 | !strcmp(node->key, "out_key")) { | |
c19e6535 | 501 | /* Handled separately below. */ |
526df7d8 TG |
502 | } else if (!strcmp(node->key, "exts")) { |
503 | char *str = xstrdup(node->value); | |
504 | char *ext, *save_ptr = NULL; | |
505 | ||
506 | tnl_cfg.exts = 0; | |
507 | ||
508 | ext = strtok_r(str, ",", &save_ptr); | |
509 | while (ext) { | |
510 | if (!strcmp(type, "vxlan") && !strcmp(ext, "gbp")) { | |
511 | tnl_cfg.exts |= (1 << OVS_VXLAN_EXT_GBP); | |
439f39cb GS |
512 | } else if (!strcmp(type, "vxlan") && !strcmp(ext, "gpe")) { |
513 | tnl_cfg.exts |= (1 << OVS_VXLAN_EXT_GPE); | |
514 | optional_layer3 = true; | |
526df7d8 | 515 | } else { |
9fff138e DDP |
516 | ds_put_format(&errors, "%s: unknown extension '%s'\n", |
517 | name, ext); | |
526df7d8 TG |
518 | } |
519 | ||
520 | ext = strtok_r(NULL, ",", &save_ptr); | |
521 | } | |
522 | ||
523 | free(str); | |
bf4bbd0d PS |
524 | } else if (!strcmp(node->key, "egress_pkt_mark")) { |
525 | tnl_cfg.egress_pkt_mark = strtoul(node->value, NULL, 10); | |
526 | tnl_cfg.set_egress_pkt_mark = true; | |
439f39cb | 527 | } else if (!strcmp(node->key, "layer3")) { |
63171f04 | 528 | if (!strcmp(node->value, "true")) { |
439f39cb | 529 | is_layer3 = true; |
63171f04 | 530 | } |
2b9d6589 | 531 | } else { |
439f39cb GS |
532 | ds_put_format(&errors, "%s: unknown %s argument '%s'\n", name, |
533 | type, node->key); | |
2b9d6589 BP |
534 | } |
535 | } | |
536 | ||
439f39cb GS |
537 | if (optional_layer3 && is_layer3) { |
538 | tnl_cfg.is_layer3 = is_layer3; | |
539 | } else if (!optional_layer3 && is_layer3) { | |
540 | ds_put_format(&errors, "%s: unknown %s argument '%s'\n", | |
541 | name, type, "layer3"); | |
542 | } | |
543 | ||
3ae91c01 | 544 | if (!ipv6_addr_is_set(&tnl_cfg.ipv6_dst) && !tnl_cfg.ip_dst_flow) { |
9fff138e DDP |
545 | ds_put_format(&errors, |
546 | "%s: %s type requires valid 'remote_ip' argument\n", | |
547 | name, type); | |
548 | err = EINVAL; | |
549 | goto out; | |
2b9d6589 | 550 | } |
0ad90c84 | 551 | if (tnl_cfg.ip_src_flow && !tnl_cfg.ip_dst_flow) { |
9fff138e DDP |
552 | ds_put_format(&errors, |
553 | "%s: %s type requires 'remote_ip=flow' " | |
554 | "with 'local_ip=flow'\n", | |
555 | name, type); | |
556 | err = EINVAL; | |
557 | goto out; | |
0ad90c84 | 558 | } |
3ae91c01 | 559 | if (src_proto && dst_proto && src_proto != dst_proto) { |
9fff138e DDP |
560 | ds_put_format(&errors, |
561 | "%s: 'remote_ip' and 'local_ip' " | |
562 | "has to be of the same address family\n", | |
563 | name); | |
564 | err = EINVAL; | |
565 | goto out; | |
3ae91c01 | 566 | } |
f431bf7d EJ |
567 | if (!tnl_cfg.ttl) { |
568 | tnl_cfg.ttl = DEFAULT_TTL; | |
569 | } | |
570 | ||
571 | tnl_cfg.in_key = parse_key(args, "in_key", | |
572 | &tnl_cfg.in_key_present, | |
573 | &tnl_cfg.in_key_flow); | |
f431bf7d EJ |
574 | |
575 | tnl_cfg.out_key = parse_key(args, "out_key", | |
576 | &tnl_cfg.out_key_present, | |
577 | &tnl_cfg.out_key_flow); | |
2b9d6589 | 578 | |
86383816 | 579 | ovs_mutex_lock(&dev->mutex); |
a1908399 AW |
580 | if (memcmp(&dev->tnl_cfg, &tnl_cfg, sizeof tnl_cfg)) { |
581 | dev->tnl_cfg = tnl_cfg; | |
582 | tunnel_check_status_change__(dev); | |
583 | netdev_change_seq_changed(dev_); | |
584 | } | |
86383816 | 585 | ovs_mutex_unlock(&dev->mutex); |
f431bf7d | 586 | |
9fff138e DDP |
587 | err = 0; |
588 | ||
589 | out: | |
c296d3f8 DDP |
590 | if (errors.length) { |
591 | ds_chomp(&errors, '\n'); | |
592 | VLOG_WARN("%s", ds_cstr(&errors)); | |
593 | if (err) { | |
594 | *errp = ds_steal_cstr(&errors); | |
595 | } | |
9fff138e DDP |
596 | } |
597 | ||
598 | ds_destroy(&errors); | |
599 | ||
600 | return err; | |
c19e6535 BP |
601 | } |
602 | ||
2b9d6589 | 603 | static int |
b5d57fc8 | 604 | get_tunnel_config(const struct netdev *dev, struct smap *args) |
6d9e6eb4 | 605 | { |
86383816 | 606 | struct netdev_vport *netdev = netdev_vport_cast(dev); |
63171f04 | 607 | const char *type = netdev_get_type(dev); |
86383816 BP |
608 | struct netdev_tunnel_config tnl_cfg; |
609 | ||
610 | ovs_mutex_lock(&netdev->mutex); | |
611 | tnl_cfg = netdev->tnl_cfg; | |
612 | ovs_mutex_unlock(&netdev->mutex); | |
6d9e6eb4 | 613 | |
3ae91c01 JB |
614 | if (ipv6_addr_is_set(&tnl_cfg.ipv6_dst)) { |
615 | smap_add_ipv6(args, "remote_ip", &tnl_cfg.ipv6_dst); | |
86383816 | 616 | } else if (tnl_cfg.ip_dst_flow) { |
0ad90c84 | 617 | smap_add(args, "remote_ip", "flow"); |
0a740f48 EJ |
618 | } |
619 | ||
3ae91c01 JB |
620 | if (ipv6_addr_is_set(&tnl_cfg.ipv6_src)) { |
621 | smap_add_ipv6(args, "local_ip", &tnl_cfg.ipv6_src); | |
86383816 | 622 | } else if (tnl_cfg.ip_src_flow) { |
0ad90c84 | 623 | smap_add(args, "local_ip", "flow"); |
7f804ea5 | 624 | } |
c19e6535 | 625 | |
86383816 | 626 | if (tnl_cfg.in_key_flow && tnl_cfg.out_key_flow) { |
6d9e6eb4 | 627 | smap_add(args, "key", "flow"); |
86383816 BP |
628 | } else if (tnl_cfg.in_key_present && tnl_cfg.out_key_present |
629 | && tnl_cfg.in_key == tnl_cfg.out_key) { | |
630 | smap_add_format(args, "key", "%"PRIu64, ntohll(tnl_cfg.in_key)); | |
6d9e6eb4 | 631 | } else { |
86383816 | 632 | if (tnl_cfg.in_key_flow) { |
b9ad7294 | 633 | smap_add(args, "in_key", "flow"); |
86383816 | 634 | } else if (tnl_cfg.in_key_present) { |
b9ad7294 | 635 | smap_add_format(args, "in_key", "%"PRIu64, |
86383816 | 636 | ntohll(tnl_cfg.in_key)); |
b9ad7294 | 637 | } |
6d9e6eb4 | 638 | |
86383816 | 639 | if (tnl_cfg.out_key_flow) { |
b9ad7294 | 640 | smap_add(args, "out_key", "flow"); |
86383816 | 641 | } else if (tnl_cfg.out_key_present) { |
b9ad7294 | 642 | smap_add_format(args, "out_key", "%"PRIu64, |
86383816 | 643 | ntohll(tnl_cfg.out_key)); |
6d9e6eb4 BP |
644 | } |
645 | } | |
646 | ||
86383816 | 647 | if (tnl_cfg.ttl_inherit) { |
62827e6a | 648 | smap_add(args, "ttl", "inherit"); |
86383816 BP |
649 | } else if (tnl_cfg.ttl != DEFAULT_TTL) { |
650 | smap_add_format(args, "ttl", "%"PRIu8, tnl_cfg.ttl); | |
c19e6535 BP |
651 | } |
652 | ||
86383816 | 653 | if (tnl_cfg.tos_inherit) { |
6d9e6eb4 | 654 | smap_add(args, "tos", "inherit"); |
86383816 BP |
655 | } else if (tnl_cfg.tos) { |
656 | smap_add_format(args, "tos", "0x%x", tnl_cfg.tos); | |
6d9e6eb4 BP |
657 | } |
658 | ||
86383816 BP |
659 | if (tnl_cfg.dst_port) { |
660 | uint16_t dst_port = ntohs(tnl_cfg.dst_port); | |
9eeb949b | 661 | |
c1fc1411 JG |
662 | if ((!strcmp("geneve", type) && dst_port != GENEVE_DST_PORT) || |
663 | (!strcmp("vxlan", type) && dst_port != VXLAN_DST_PORT) || | |
4237026e PS |
664 | (!strcmp("lisp", type) && dst_port != LISP_DST_PORT) || |
665 | (!strcmp("stt", type) && dst_port != STT_DST_PORT)) { | |
79f827fa KM |
666 | smap_add_format(args, "dst_port", "%d", dst_port); |
667 | } | |
668 | } | |
669 | ||
86383816 | 670 | if (tnl_cfg.csum) { |
6d9e6eb4 BP |
671 | smap_add(args, "csum", "true"); |
672 | } | |
8a9ff93a | 673 | |
439f39cb GS |
674 | if (tnl_cfg.is_layer3 && (!strcmp("gre", type) || |
675 | !strcmp("vxlan", type))) { | |
63171f04 JS |
676 | smap_add(args, "layer3", "true"); |
677 | } | |
678 | ||
86383816 | 679 | if (!tnl_cfg.dont_fragment) { |
66409d1b AE |
680 | smap_add(args, "df_default", "false"); |
681 | } | |
6d9e6eb4 | 682 | |
bf4bbd0d PS |
683 | if (tnl_cfg.set_egress_pkt_mark) { |
684 | smap_add_format(args, "egress_pkt_mark", | |
685 | "%"PRIu32, tnl_cfg.egress_pkt_mark); | |
686 | } | |
6d9e6eb4 BP |
687 | return 0; |
688 | } | |
0a740f48 EJ |
689 | \f |
690 | /* Code specific to patch ports. */ | |
691 | ||
161b6042 BP |
692 | /* If 'netdev' is a patch port, returns the name of its peer as a malloc()'d |
693 | * string that the caller must free. | |
694 | * | |
695 | * If 'netdev' is not a patch port, returns NULL. */ | |
696 | char * | |
697 | netdev_vport_patch_peer(const struct netdev *netdev_) | |
0a740f48 | 698 | { |
161b6042 BP |
699 | char *peer = NULL; |
700 | ||
701 | if (netdev_vport_is_patch(netdev_)) { | |
702 | struct netdev_vport *netdev = netdev_vport_cast(netdev_); | |
86383816 BP |
703 | |
704 | ovs_mutex_lock(&netdev->mutex); | |
161b6042 BP |
705 | if (netdev->peer) { |
706 | peer = xstrdup(netdev->peer); | |
707 | } | |
86383816 | 708 | ovs_mutex_unlock(&netdev->mutex); |
161b6042 BP |
709 | } |
710 | ||
711 | return peer; | |
0a740f48 EJ |
712 | } |
713 | ||
714 | void | |
b9ad7294 | 715 | netdev_vport_inc_rx(const struct netdev *netdev, |
9e04d6f6 | 716 | const struct dpif_flow_stats *stats) |
0a740f48 | 717 | { |
b5d57fc8 BP |
718 | if (is_vport_class(netdev_get_class(netdev))) { |
719 | struct netdev_vport *dev = netdev_vport_cast(netdev); | |
86383816 BP |
720 | |
721 | ovs_mutex_lock(&dev->mutex); | |
0a740f48 EJ |
722 | dev->stats.rx_packets += stats->n_packets; |
723 | dev->stats.rx_bytes += stats->n_bytes; | |
86383816 | 724 | ovs_mutex_unlock(&dev->mutex); |
0a740f48 EJ |
725 | } |
726 | } | |
727 | ||
728 | void | |
b9ad7294 EJ |
729 | netdev_vport_inc_tx(const struct netdev *netdev, |
730 | const struct dpif_flow_stats *stats) | |
0a740f48 | 731 | { |
b5d57fc8 BP |
732 | if (is_vport_class(netdev_get_class(netdev))) { |
733 | struct netdev_vport *dev = netdev_vport_cast(netdev); | |
86383816 BP |
734 | |
735 | ovs_mutex_lock(&dev->mutex); | |
0a740f48 EJ |
736 | dev->stats.tx_packets += stats->n_packets; |
737 | dev->stats.tx_bytes += stats->n_bytes; | |
86383816 | 738 | ovs_mutex_unlock(&dev->mutex); |
0a740f48 EJ |
739 | } |
740 | } | |
741 | ||
742 | static int | |
b5d57fc8 | 743 | get_patch_config(const struct netdev *dev_, struct smap *args) |
0a740f48 | 744 | { |
b5d57fc8 | 745 | struct netdev_vport *dev = netdev_vport_cast(dev_); |
0a740f48 | 746 | |
86383816 | 747 | ovs_mutex_lock(&dev->mutex); |
0a740f48 EJ |
748 | if (dev->peer) { |
749 | smap_add(args, "peer", dev->peer); | |
750 | } | |
86383816 BP |
751 | ovs_mutex_unlock(&dev->mutex); |
752 | ||
0a740f48 EJ |
753 | return 0; |
754 | } | |
6d9e6eb4 BP |
755 | |
756 | static int | |
9fff138e | 757 | set_patch_config(struct netdev *dev_, const struct smap *args, char **errp) |
2b9d6589 | 758 | { |
b5d57fc8 BP |
759 | struct netdev_vport *dev = netdev_vport_cast(dev_); |
760 | const char *name = netdev_get_name(dev_); | |
2b9d6589 BP |
761 | const char *peer; |
762 | ||
79f1cbe9 | 763 | peer = smap_get(args, "peer"); |
2b9d6589 | 764 | if (!peer) { |
9fff138e DDP |
765 | VLOG_ERR_BUF(errp, "%s: patch type requires valid 'peer' argument", |
766 | name); | |
2b9d6589 BP |
767 | return EINVAL; |
768 | } | |
769 | ||
79f1cbe9 | 770 | if (smap_count(args) > 1) { |
9fff138e DDP |
771 | VLOG_ERR_BUF(errp, "%s: patch type takes only a 'peer' argument", |
772 | name); | |
2b9d6589 BP |
773 | return EINVAL; |
774 | } | |
775 | ||
2b9d6589 | 776 | if (!strcmp(name, peer)) { |
9fff138e | 777 | VLOG_ERR_BUF(errp, "%s: patch peer must not be self", name); |
2b9d6589 BP |
778 | return EINVAL; |
779 | } | |
780 | ||
86383816 | 781 | ovs_mutex_lock(&dev->mutex); |
a1908399 AW |
782 | if (!dev->peer || strcmp(dev->peer, peer)) { |
783 | free(dev->peer); | |
784 | dev->peer = xstrdup(peer); | |
785 | netdev_change_seq_changed(dev_); | |
786 | } | |
86383816 BP |
787 | ovs_mutex_unlock(&dev->mutex); |
788 | ||
2b9d6589 BP |
789 | return 0; |
790 | } | |
6d9e6eb4 BP |
791 | |
792 | static int | |
b9ad7294 | 793 | get_stats(const struct netdev *netdev, struct netdev_stats *stats) |
0a740f48 | 794 | { |
b5d57fc8 | 795 | struct netdev_vport *dev = netdev_vport_cast(netdev); |
86383816 BP |
796 | |
797 | ovs_mutex_lock(&dev->mutex); | |
d6e3feb5 | 798 | /* Passing only collected counters */ |
799 | stats->tx_packets = dev->stats.tx_packets; | |
800 | stats->tx_bytes = dev->stats.tx_bytes; | |
801 | stats->rx_packets = dev->stats.rx_packets; | |
802 | stats->rx_bytes = dev->stats.rx_bytes; | |
86383816 BP |
803 | ovs_mutex_unlock(&dev->mutex); |
804 | ||
6d9e6eb4 BP |
805 | return 0; |
806 | } | |
a36de779 | 807 | |
2b9d6589 | 808 | \f |
0a740f48 | 809 | #define VPORT_FUNCTIONS(GET_CONFIG, SET_CONFIG, \ |
a36de779 PS |
810 | GET_TUNNEL_CONFIG, GET_STATUS, \ |
811 | BUILD_HEADER, \ | |
812 | PUSH_HEADER, POP_HEADER) \ | |
b46ccdf5 | 813 | NULL, \ |
ea83a2fc EJ |
814 | netdev_vport_run, \ |
815 | netdev_vport_wait, \ | |
2b9d6589 | 816 | \ |
9dc63482 BP |
817 | netdev_vport_alloc, \ |
818 | netdev_vport_construct, \ | |
819 | netdev_vport_destruct, \ | |
820 | netdev_vport_dealloc, \ | |
0a740f48 EJ |
821 | GET_CONFIG, \ |
822 | SET_CONFIG, \ | |
f431bf7d | 823 | GET_TUNNEL_CONFIG, \ |
a36de779 PS |
824 | BUILD_HEADER, \ |
825 | PUSH_HEADER, \ | |
826 | POP_HEADER, \ | |
7dec44fe | 827 | NULL, /* get_numa_id */ \ |
050c60bf | 828 | NULL, /* set_tx_multiq */ \ |
2b9d6589 | 829 | \ |
552e20d0 | 830 | NULL, /* send */ \ |
2b9d6589 BP |
831 | NULL, /* send_wait */ \ |
832 | \ | |
833 | netdev_vport_set_etheraddr, \ | |
834 | netdev_vport_get_etheraddr, \ | |
14622f22 BP |
835 | NULL, /* get_mtu */ \ |
836 | NULL, /* set_mtu */ \ | |
2b9d6589 | 837 | NULL, /* get_ifindex */ \ |
85da620e | 838 | NULL, /* get_carrier */ \ |
65c3058c | 839 | NULL, /* get_carrier_resets */ \ |
63331829 | 840 | NULL, /* get_miimon */ \ |
b9ad7294 | 841 | get_stats, \ |
2b9d6589 BP |
842 | \ |
843 | NULL, /* get_features */ \ | |
844 | NULL, /* set_advertisements */ \ | |
2b9d6589 BP |
845 | \ |
846 | NULL, /* set_policing */ \ | |
847 | NULL, /* get_qos_types */ \ | |
848 | NULL, /* get_qos_capabilities */ \ | |
849 | NULL, /* get_qos */ \ | |
850 | NULL, /* set_qos */ \ | |
851 | NULL, /* get_queue */ \ | |
852 | NULL, /* set_queue */ \ | |
853 | NULL, /* delete_queue */ \ | |
854 | NULL, /* get_queue_stats */ \ | |
89454bf4 BP |
855 | NULL, /* queue_dump_start */ \ |
856 | NULL, /* queue_dump_next */ \ | |
857 | NULL, /* queue_dump_done */ \ | |
2b9d6589 BP |
858 | NULL, /* dump_queue_stats */ \ |
859 | \ | |
2b9d6589 | 860 | NULL, /* set_in4 */ \ |
a8704b50 | 861 | NULL, /* get_addr_list */ \ |
2b9d6589 BP |
862 | NULL, /* add_router */ \ |
863 | NULL, /* get_next_hop */ \ | |
ea763e0e | 864 | GET_STATUS, \ |
2b9d6589 BP |
865 | NULL, /* arp_lookup */ \ |
866 | \ | |
867 | netdev_vport_update_flags, \ | |
790fb3b7 | 868 | NULL, /* reconfigure */ \ |
2b9d6589 | 869 | \ |
9dc63482 BP |
870 | NULL, /* rx_alloc */ \ |
871 | NULL, /* rx_construct */ \ | |
872 | NULL, /* rx_destruct */ \ | |
873 | NULL, /* rx_dealloc */ \ | |
874 | NULL, /* rx_recv */ \ | |
875 | NULL, /* rx_wait */ \ | |
18ebd48c PB |
876 | NULL, /* rx_drain */ \ |
877 | \ | |
878 | NULL, /* flow_flush */ \ | |
879 | NULL, /* flow_dump_create */ \ | |
880 | NULL, /* flow_dump_destroy */ \ | |
881 | NULL, /* flow_dump_next */ \ | |
882 | NULL, /* flow_put */ \ | |
883 | NULL, /* flow_get */ \ | |
884 | NULL, /* flow_del */ \ | |
885 | NULL, /* init_flow_api */ | |
2b9d6589 | 886 | |
a36de779 PS |
887 | |
888 | #define TUNNEL_CLASS(NAME, DPIF_PORT, BUILD_HEADER, PUSH_HEADER, POP_HEADER) \ | |
889 | { DPIF_PORT, \ | |
118c77b1 IM |
890 | { NAME, false, \ |
891 | VPORT_FUNCTIONS(get_tunnel_config, \ | |
892 | set_tunnel_config, \ | |
893 | get_netdev_tunnel_config, \ | |
894 | tunnel_get_status, \ | |
895 | BUILD_HEADER, PUSH_HEADER, POP_HEADER) }} | |
db078f85 | 896 | |
2b9d6589 | 897 | void |
c060c4cf | 898 | netdev_vport_tunnel_register(void) |
2b9d6589 | 899 | { |
a5d4fadd JG |
900 | /* The name of the dpif_port should be short enough to accomodate adding |
901 | * a port number to the end if one is necessary. */ | |
c3827f61 | 902 | static const struct vport_class vport_classes[] = { |
e5a1caee | 903 | TUNNEL_CLASS("geneve", "genev_sys", netdev_geneve_build_header, |
6b241d64 | 904 | netdev_tnl_push_udp_header, |
e5a1caee | 905 | netdev_geneve_pop_header), |
a36de779 PS |
906 | TUNNEL_CLASS("gre", "gre_sys", netdev_gre_build_header, |
907 | netdev_gre_push_header, | |
908 | netdev_gre_pop_header), | |
a36de779 | 909 | TUNNEL_CLASS("vxlan", "vxlan_sys", netdev_vxlan_build_header, |
6b241d64 | 910 | netdev_tnl_push_udp_header, |
a36de779 | 911 | netdev_vxlan_pop_header), |
4237026e PS |
912 | TUNNEL_CLASS("lisp", "lisp_sys", NULL, NULL, NULL), |
913 | TUNNEL_CLASS("stt", "stt_sys", NULL, NULL, NULL), | |
c3827f61 | 914 | }; |
86383816 | 915 | static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER; |
c3827f61 | 916 | |
86383816 BP |
917 | if (ovsthread_once_start(&once)) { |
918 | int i; | |
c3827f61 | 919 | |
7c54c27f BP |
920 | for (i = 0; i < ARRAY_SIZE(vport_classes); i++) { |
921 | netdev_register_provider(&vport_classes[i].netdev_class); | |
922 | } | |
a36de779 PS |
923 | |
924 | unixctl_command_register("tnl/egress_port_range", "min max", 0, 2, | |
6b241d64 | 925 | netdev_tnl_egress_port_range, NULL); |
a36de779 | 926 | |
86383816 | 927 | ovsthread_once_done(&once); |
c3827f61 | 928 | } |
2b9d6589 | 929 | } |
c060c4cf EJ |
930 | |
931 | void | |
932 | netdev_vport_patch_register(void) | |
933 | { | |
934 | static const struct vport_class patch_class = | |
935 | { NULL, | |
118c77b1 IM |
936 | { "patch", false, |
937 | VPORT_FUNCTIONS(get_patch_config, | |
938 | set_patch_config, | |
939 | NULL, | |
940 | NULL, NULL, NULL, NULL) }}; | |
c060c4cf EJ |
941 | netdev_register_provider(&patch_class.netdev_class); |
942 | } |