BUG 6467: Add cli flag to use virtual environment
[ovsdb.git] / utils / ovsdb-it-utils / src / main / java / org / opendaylight / ovsdb / utils / ovsdb / it / utils / DockerOvs.java
1 /*
2  * Copyright (c) 2016 Red Hat, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8
9 package org.opendaylight.ovsdb.utils.ovsdb.it.utils;
10
11 import java.io.File;
12 import java.io.FileNotFoundException;
13 import java.io.FileReader;
14 import java.io.FileWriter;
15 import java.io.InputStreamReader;
16 import java.io.IOException;
17 import java.io.Reader;
18 import java.net.InetSocketAddress;
19 import java.net.URL;
20 import java.nio.ByteBuffer;
21 import java.nio.channels.ClosedByInterruptException;
22 import java.nio.channels.SocketChannel;
23 import java.nio.charset.Charset;
24 import java.nio.charset.CharsetDecoder;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Properties;
30 import java.util.concurrent.atomic.AtomicInteger;
31
32 import com.esotericsoftware.yamlbeans.YamlException;
33 import com.esotericsoftware.yamlbeans.YamlReader;
34 import org.junit.Assert;
35 import org.ops4j.pax.exam.Option;
36 import org.osgi.framework.Bundle;
37 import org.osgi.framework.FrameworkUtil;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import static org.ops4j.pax.exam.CoreOptions.propagateSystemProperties;
42
43 /**
44  * Run OVS(s) using docker-compose for use in integration tests.
45  * For example,
46  * <pre>
47  * try(DockerOvs ovs = new DockerOvs()) {
48  *      ConnectionInfo connectionInfo = SouthboundUtils.getConnectionInfo(
49  *                               ovs.getOvsdbAddress(0), ovs.getOvsdbPort(0));
50  *      ...
51  *       nodeInfo.disconnect();
52  *
53  * } catch (Exception e) {
54  * ...
55  * </pre>
56  * <b>
57  * Nota bene, DockerOvs will check whether or not docker-compose command requires "sudo"
58  * to run. However, if it does require sudo, it must be configured to not prompt for a
59  * password ("NOPASSWD: ALL" is the sudoers file).
60  * </b>
61  * DockerOvs loads its docker-compose yaml files from inside the ovsdb-it-utils bundle
62  * at the path META-INF/docker-compose-files/. Currently, a single yaml file is used,
63  * "docker-ovs-2.5.1.yml." DockerOvs does support docker-compose files that
64  * launch more than one docker image, more on this later. DockerOvs will wait for OVS
65  * to accept OVSDB connections.
66  * Any docker-compose file must have a port mapping.
67  *
68  * The following explains how system properties are used to configure DockerOvs
69  * <pre>
70  *  private static String ENV_USAGE =
71  *  "-Ddocker.run - explicitly configure whether or not DockerOvs should run docker-compose\n" +
72  *  "-Dovsdbserver.ipaddress - specify IP address of ovsdb server - implies -Ddocker.run=false\n" +
73  *  "-Dovsdbserver.port - specify the port of the ovsdb server - required with -Dovsdbserver.ipaddress\n" +
74  *  "-Ddocker.compose.file - docker compose file in META-INF/docker-compose-files/. If not specified, default file is used\n" +
75  *  "-Dovsdb.userspace.enabled - true when Ovs is running in user space (usually the case with docker)\n" +
76  *  "-Dovsdb.controller.address - IP address of the controller (usually the docker0 interface with docker)\n" +
77  *  "To auto-run Ovs and connect actively:\n" +
78  *  " -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes [-Ddocker.compose.file=ffff]\n" +
79  *  "To auto-run Ovs and connect passively:\n" +
80  *  " -Dovsdbserver.connection=passive -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes [-Ddocker.compose.file=ffff]\n" +
81  *  "To actively connect to a running Ovs:\n" +
82  *  " -Dovsdbserver.ipaddress=x.x.x.x -Dovsdbserver.port=6641 -Dovsdb.controller.address=y.y.y.y\n" +
83  *  "To passively connect to a running Ovs:\n" +
84  *  " -Dovsdbserver.connection=passive -Ddocker.run=false\n";
85  * </pre>
86  * When DockerOvs does not run docker-compose getOvsdbAddress and getOvsdbPort return the address and port specified in
87  * the system properties.
88  */
89 public class DockerOvs implements AutoCloseable {
90     private static String ENV_USAGE = "Usage:\n" +
91             "-Ddocker.run - explicitly configure whether or not DockerOvs should run docker-compose\n" +
92                     "-Dovsdbserver.ipaddress - specify IP address of ovsdb server - implies -Ddocker.run=false\n" +
93                     "-Dovsdbserver.port - specify the port of the ovsdb server - required with -Dovsdbserver.ipaddress\n" +
94                     "-Ddocker.compose.file - docker compose file in META-INF/docker-compose-files/. If not specified, default file is used\n" +
95                     "-Dovsdb.userspace.enabled - true when Ovs is running in user space (usually the case with docker)\n" +
96                     "-Dovsdb.controller.address - IP address of the controller (usually the docker0 interface with docker)\n" +
97                     "To auto-run Ovs and connect actively:\n" +
98                     " -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes <-Ddocker.compose.file=ffff>\n" +
99                     "To auto-run Ovs and connect passively:\n" +
100                     " -Dovsdbserver.connection=passive -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes <-Ddocker.compose.file=ffff>\n" +
101                     "To actively connect to a running Ovs:\n" +
102                     " -Dovsdbserver.ipaddress=x.x.x.x -Dovsdbserver.port=6641 -Dovsdb.controller.address=y.y.y.y\n" +
103                     "To passively connect to a running Ovs:\n" +
104                     " -Dovsdbserver.connection=passive -Ddocker.run=false\n";
105
106     private static final Logger LOG = LoggerFactory.getLogger(DockerOvs.class);
107     private static final String DEFAULT_DOCKER_FILE = "ovs-2.5.0-hwvtep.yml";
108     private static final String DOCKER_FILE_PATH = "META-INF/docker-compose-files/";
109     private static final int COMPOSE_FILE_IDX = 3;
110     private static final int COMPOSE_FILE_IDX_NO_SUDO = 2;
111     private static final String DEFAULT_OVSDB_HOST = "127.0.0.1";
112
113     private String[] psCmd = {"sudo", "docker-compose", "-f", null, "ps"};
114     private String[] psCmdNoSudo = {"docker-compose", "-f", null, "ps"};
115     private String[] upCmd = {"sudo", "docker-compose", "-f", null, "up", "-d", "--force-recreate"};
116     private String[] downCmd = {"sudo", "docker-compose", "-f", null, "stop"};
117     private String[] execCmd = {"sudo", "docker-compose", "-f", null, "exec", null};
118
119     private File tmpDockerComposeFile;
120     boolean isRunning;
121     private String envServerAddress;
122     private String envServerPort;
123     private String envDockerComposeFile;
124     private boolean runDocker;
125     private boolean runVenv;
126
127     class DockerComposeServiceInfo {
128         public String name;
129         public String port;
130     }
131     private List<DockerComposeServiceInfo> dockerComposeServices = new ArrayList<DockerComposeServiceInfo>();
132
133     /**
134      * Get the array of system properties as pax exam Option objects for use in pax exam
135      * unit tests with Configuration annotation.
136      * @return List of Option objects
137      */
138     public static Option[] getSysPropOptions() {
139         return new Option[] {
140                 propagateSystemProperties(ItConstants.SERVER_IPADDRESS,
141                                             ItConstants.SERVER_PORT,
142                                             ItConstants.CONNECTION_TYPE,
143                                             ItConstants.CONTROLLER_IPADDRESS,
144                                             ItConstants.USERSPACE_ENABLED,
145                                             ItConstants.DOCKER_COMPOSE_FILE_NAME,
146                                             ItConstants.DOCKER_RUN,
147                                             ItConstants.DOCKER_VENV_WS)
148         };
149     }
150
151     /**
152      * Bring up all docker images in the default docker-compose file.
153      * @throws IOException if something goes wrong on the IO end
154      * @throws InterruptedException If this thread is interrupted
155      */
156     public DockerOvs() throws IOException, InterruptedException {
157         this(DEFAULT_DOCKER_FILE);
158     }
159
160     /**
161      * Bring up all docker images in the provided docker-compose file under "META-INF/docker-compose-files/".
162      * @param yamlFileName Just the file name
163      * @throws IOException if something goes wrong on the IO end
164      * @throws InterruptedException If this thread is interrupted
165      */
166     public DockerOvs(String yamlFileName) throws IOException, InterruptedException {
167         configureFromEnv();
168
169         if (!runDocker) {
170             LOG.info("DockerOvs.DockerOvs: Not running docker, -D{} specified", ItConstants.SERVER_IPADDRESS);
171             return;
172         }
173
174         tmpDockerComposeFile = createTempDockerComposeFile(yamlFileName);
175         buildDockerComposeCommands();
176         parseDockerComposeYaml();
177
178         isRunning = false;
179         //We run this for A LONG TIME since on the first run docker must download the
180         //image from docker hub. In experience it takes significantly less than this
181         //even when downloading the image. Once the image is downloaded this command
182         //runs like that <snaps fingers>
183         ProcUtils.runProcess(60000, upCmd);
184         isRunning = true;
185         waitForOvsdbServers(10 * 1000);
186     }
187
188     private void setupEnvForDockerCompose(String venvWs) {
189         String dockerCompose = venvWs + "/venv/bin/docker-compose";
190         String[] psCmdVenv = {"sudo", dockerCompose, "-f", null, "ps"};
191         String[] psCmdNoSudoVenv = {dockerCompose, "-f", null, "ps"};
192         String[] upCmdVenv = {"sudo", dockerCompose, "-f", null, "up", "-d", "--force-recreate"};
193         String[] downCmdVenv = {"sudo", dockerCompose, "-f", null, "stop"};
194         String[] execCmdVenv = {"sudo", dockerCompose, "-f", null, "exec", null};
195
196         psCmd = psCmdVenv;
197         psCmdNoSudo = psCmdNoSudoVenv;
198         upCmd = upCmdVenv;
199         downCmd = downCmdVenv;
200         execCmd = execCmdVenv;
201     }
202
203     /**
204      * Pull required configuration from System.getProperties() and validate we have what we need.
205      * Note: Note that there is some minor complexity in how this class is configured using System
206      * properties. This stems from the fact that we want to preserve the meaning of these properties
207      * prior to the introduction of this class. See the ENV_USAGE variable for details.
208      */
209     private void configureFromEnv() {
210         Properties env = System.getProperties();
211         envServerAddress = env.getProperty(ItConstants.SERVER_IPADDRESS);
212         envServerPort = env.getProperty(ItConstants.SERVER_PORT);
213         String envRunDocker = env.getProperty(ItConstants.DOCKER_RUN);
214         String connType = env.getProperty(ItConstants.CONNECTION_TYPE, ItConstants.CONNECTION_TYPE_ACTIVE);
215         String dockerFile = env.getProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME);
216         String venvWs = env.getProperty(ItConstants.DOCKER_VENV_WS);
217         envDockerComposeFile = DOCKER_FILE_PATH + (null == dockerFile ? DEFAULT_DOCKER_FILE : dockerFile);
218
219         //Are we running docker? If we specified docker.run, that's the answer. Otherwise, if there is a server
220         //address we assume docker is already running
221         runDocker = (envRunDocker != null) ? Boolean.parseBoolean(envRunDocker) : envServerAddress == null;
222
223         //Should we run in a virtual environment? Needed for jenkins and docker-compose
224         if ((venvWs != null) && (!venvWs.isEmpty())) {
225             LOG.info("Setting up virtual environment for docker compose");
226             setupEnvForDockerCompose(venvWs);
227         }
228
229         if(runDocker) {
230             return;
231         }
232
233         if (connType.equals(ItConstants.CONNECTION_TYPE_PASSIVE)) {
234             return;
235         }
236
237         //At this point we know we're not running docker and the conn type is active - make sure we have what we need
238         //If we have a server address than we require a port too as those
239         //are returned in getOvsdbPort() and getOvsdbAddress()
240         Assert.assertNotNull("Attempt to connect to previous running ovs but missing -Dovsdbserver.ipaddress\n"
241                                                                                     + ENV_USAGE, envServerAddress);
242         Assert.assertNotNull("Attempt to connect to previous running ovs but missing -Dovsdbserver.port\n"
243                 + ENV_USAGE, envServerPort);
244     }
245
246     /**
247      * Verify and build the docker-compose commands we will be running. This function adds the docker-compose file
248      * to the command lines and also checks (and adjusts the command line) as to whether sudo is required. This is
249      * done by attempting to run "docker-compose ps" without and then with sudo
250      * @throws IOException if something goes wrong on the IO end
251      * @throws InterruptedException If this thread is interrupted
252      */
253     private void buildDockerComposeCommands() throws IOException, InterruptedException {
254         psCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
255         psCmdNoSudo[COMPOSE_FILE_IDX_NO_SUDO] = tmpDockerComposeFile.toString();
256         upCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
257         downCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
258         execCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
259
260         if (0 == ProcUtils.tryProcess(5000, psCmdNoSudo)) {
261             LOG.info("DockerOvs.buildDockerComposeCommands docker-compose does not require sudo");
262             String[] tmp;
263             tmp = Arrays.copyOfRange(upCmd, 1, upCmd.length);
264             upCmd = tmp;
265             tmp = Arrays.copyOfRange(downCmd, 1, downCmd.length);
266             downCmd = tmp;
267             tmp = Arrays.copyOfRange(execCmd, 1, execCmd.length);
268             execCmd = tmp;
269         } else if (0 == ProcUtils.tryProcess(5000, psCmd)) {
270             LOG.info("DockerOvs.buildDockerComposeCommands docker-compose requires sudo");
271         } else {
272             Assert.fail("docker-compose does not seem to work with or without sudo");
273         }
274     }
275
276     /**
277      * Are we using some other OVS, not a docker we spin up?
278      * @return true if we are *not* running a docker image to test against
279      */
280     public boolean usingExternalDocker() {
281         return !runDocker;
282     }
283
284     /**
285      * Get the IP address of the n'th OVS.
286      * @param ovsNumber which OVS?
287      * @return IP string
288      */
289     public String getOvsdbAddress(int ovsNumber) {
290         if (!runDocker) {
291             return envServerAddress;
292         }
293         return DEFAULT_OVSDB_HOST;
294     }
295
296     /**
297      * Get the port of the n'th OVS.
298      * @param ovsNumber which OVS?
299      * @return Port as a string
300      */
301     public String getOvsdbPort(int ovsNumber) {
302         if (!runDocker) {
303             return envServerPort;
304         }
305         return dockerComposeServices.get(ovsNumber).port;
306     }
307
308     /**
309      * How many OVS nodes are there.
310      * @return number of running OVS nodes
311      */
312     public int getNumOvsNodes() {
313         return dockerComposeServices.size();
314     }
315
316     public String[] getExecCmdPrefix(int numOvs) {
317         String[] res = new String[execCmd.length];
318         System.arraycopy(execCmd, 0, res, 0, execCmd.length);
319         res[res.length - 1] = dockerComposeServices.get(numOvs).name;
320         return res;
321     }
322
323     public void runInContainer(int waitFor, int numOvs, String ... cmdWords) throws IOException, InterruptedException {
324         String[] pfx = getExecCmdPrefix(numOvs);
325         String[] cmd = new String[pfx.length + cmdWords.length];
326         System.arraycopy(pfx, 0, cmd, 0, pfx.length);
327         System.arraycopy(cmdWords, 0, cmd, pfx.length, cmdWords.length);
328         ProcUtils.runProcess(waitFor, cmd);
329     }
330
331     public void tryInContainer(int waitFor, int numOvs, String ... cmdWords) throws IOException, InterruptedException {
332         String[] pfx = getExecCmdPrefix(numOvs);
333         String[] cmd = new String[pfx.length + cmdWords.length];
334         System.arraycopy(pfx, 0, cmd, 0, pfx.length);
335         System.arraycopy(cmdWords, 0, cmd, pfx.length, cmdWords.length);
336         ProcUtils.tryProcess(waitFor, cmd);
337     }
338
339     /**
340      * Parse the docker-compose yaml file to extract the port mappings.
341      * @return a list of the external ports
342      */
343     private List<String> parseDockerComposeYaml() {
344         List<String> ports = new ArrayList<String>();
345
346         YamlReader yamlReader = null;
347         Map root = null;
348         try {
349             yamlReader = new YamlReader(new FileReader(tmpDockerComposeFile));
350             root = (Map) yamlReader.read();
351         } catch (FileNotFoundException e) {
352             LOG.warn("DockerOvs.parseDockerComposeYaml error reading yaml file", e);
353             return ports;
354         } catch (YamlException e) {
355             LOG.warn("DockerOvs.parseDockerComposeYaml error parsing yaml file", e);
356             return ports;
357         }
358
359         if (null == root) {
360             return ports;
361         }
362         for (Object entry : root.entrySet()) {
363             String key = ((Map.Entry<String,Map>)entry).getKey();
364             Map map = ((Map.Entry<String,Map>)entry).getValue();
365
366             DockerComposeServiceInfo svc = new DockerComposeServiceInfo();
367             svc.name = key;
368
369             List portMappings = (List) map.get("ports");
370             if (null == portMappings) {
371                 continue;
372             }
373             for (Object portMapping : portMappings) {
374                 String portMappingStr = (String) portMapping;
375                 int delim = portMappingStr.indexOf(":");
376                 if (delim == -1) {
377                     continue;
378                 }
379                 String port = portMappingStr.substring(0, delim);
380                 ports.add(port);
381                 svc.port = port;
382             }
383             //TODO: think this through. What if there is no port?
384             dockerComposeServices.add(svc);
385         }
386
387         return ports;
388     }
389
390     /**
391      * Shut everything down.
392      * @throws Exception but not really
393      */
394     @Override
395     public void close() throws Exception {
396         if (isRunning) {
397             ProcUtils.runProcess(10000, downCmd);
398             isRunning = false;
399         }
400
401         try {
402             tmpDockerComposeFile.delete();
403         } catch (Exception ignored) {
404             //No reason to fail the test, we're just being polite here.
405         }
406     }
407
408     /**
409      * A thread that waits until it can "ping" a running OVS -  tests basic reachability
410      * and readiness. The "ping" here is actually a list_dbs method and the response is
411      * checked to make sure the Open_Vswitch DB is present. Note that this thread will
412      * run until it succeeds unless its interrupt() method is called.
413      */
414     class OvsdbPing extends Thread {
415
416         private final String host;
417         private final int port;
418         private final AtomicInteger result;
419         public CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
420         ByteBuffer listDbsRequest;
421
422         /**
423          * Construct a new OvsdbPing object.
424          * @param ovsNumber which OVS is this?
425          * @param result an AtomicInteger that is incremented upon a successful "ping"
426          */
427         public OvsdbPing(int ovsNumber, AtomicInteger result) {
428             this.host = getOvsdbAddress(ovsNumber);
429             this.port = Integer.parseInt(getOvsdbPort(ovsNumber));
430             this.result = result;
431             listDbsRequest = ByteBuffer.wrap(
432                     ("{\"method\": \"list_dbs\", \"params\": [], \"id\": " + port + "}").getBytes());
433             listDbsRequest.mark();
434         }
435
436         @Override
437         public void run() {
438             while (!doPing()) {
439                 try {
440                     Thread.sleep(1000);
441                 } catch (InterruptedException e) {
442                     LOG.warn("OvsdbPing interrupted", e);
443                     return;
444                 }
445             }
446         }
447
448         /**
449          * Attempt a "ping" of the OVSDB connection.
450          * @return true if the ping was successful OR IF THIS THREAD WAS INTERRUPTED
451          */
452         private boolean doPing() {
453             try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port))) {
454                 socketChannel.write(listDbsRequest);
455                 listDbsRequest.reset();
456
457                 ByteBuffer buf = ByteBuffer.allocateDirect(512);
458                 socketChannel.read(buf);
459                 buf.flip();
460                 String response = decoder.decode(buf).toString();
461
462                 if (response.contains("Open_vSwitch")) {
463                     LOG.info("OvsdbPing connection validated");
464                     result.incrementAndGet();
465                     return true;
466                 }
467             } catch (ClosedByInterruptException e) {
468                 LOG.warn("OvsdbPing interrupted", e);
469                 //return true here because we're done, ne'er to return again.
470                 return true;
471             } catch (Exception e) {
472                 LOG.info("OvsdbPing exception while attempting connect {}", e.toString());
473             }
474             return false;
475         }
476     }
477
478     /**
479      * Wait for all Ovs's to accept and respond to OVSDB requests.
480      * @param waitFor How long to wait
481      * @throws IOException if something goes wrong on the IO end
482      * @throws InterruptedException If this thread is interrupted
483      */
484     private void waitForOvsdbServers(long waitFor) throws IOException, InterruptedException {
485         AtomicInteger numRunningOvs = new AtomicInteger(0);
486
487         int numOvs = dockerComposeServices.size();
488         if (0 == numOvs) {
489             return;
490         }
491
492         OvsdbPing[] pingers = new OvsdbPing[numOvs];
493         for (int i = 0; i < numOvs; i++) {
494             pingers[i] = new OvsdbPing(i, numRunningOvs);
495             pingers[i].start();
496         }
497
498         long startTime = System.currentTimeMillis();
499         while ( (System.currentTimeMillis() - startTime) < waitFor) {
500             if (numRunningOvs.get() >= numOvs) {
501                 LOG.info("DockerOvs.waitForOvsdbServers all OVS instances running");
502                 break;
503             }
504             Thread.sleep(1000);
505         }
506         LOG.info("DockerOvs.waitForOvsdbServers - finished waiting in {}", System.currentTimeMillis() - startTime);
507
508         for (OvsdbPing pinger : pingers) {
509             pinger.interrupt();
510         }
511     }
512
513     /**
514      * Since the docker-compose file is a resource in the bundle and docker-compose needs it.
515      * in the file system, we copy it over - ugly but necessary.
516      * @param yamlFileName File name
517      * @return A File object for the newly created temporary yaml file.
518      */
519     private File createTempDockerComposeFile(String yamlFileName) {
520         Bundle bundle = FrameworkUtil.getBundle(this.getClass());
521         Assert.assertNotNull("DockerOvs: bundle is null", bundle);
522         URL url = bundle.getResource(envDockerComposeFile);
523         Assert.assertNotNull("DockerOvs: URL is null", url);
524
525         File tmpFile = null;
526         try {
527             tmpFile = File.createTempFile("ovsdb-it-tmp-", null);
528
529             try (Reader in = new InputStreamReader(url.openStream());
530                                 FileWriter out = new FileWriter(tmpFile)) {
531                 char[] buf = new char[1024];
532                 int read;
533                 while (-1 != (read = in.read(buf))) {
534                     out.write(buf, 0, read);
535                 }
536             }
537
538         } catch (IOException e) {
539             Assert.fail(e.toString());
540         }
541
542         return tmpFile;
543     }
544
545     /**
546      * Useful for debugging. Dump some interesting config
547      * @throws IOException If something goes wrong with reading the process output
548      * @throws InterruptedException because there's some sleeping in here
549      */
550     public void logState(int dockerInstance) throws IOException, InterruptedException {
551         tryInContainer(5000, dockerInstance, "ip", "addr");
552         tryInContainer(5000, dockerInstance, "ovs-vsctl", "show");
553         tryInContainer(5000, dockerInstance, "ovs-ofctl", "-OOpenFlow13", "show", "br-int");
554         tryInContainer(5000, dockerInstance, "ovs-ofctl", "-OOpenFlow13", "dump-flows", "br-int");
555         tryInContainer(5000, dockerInstance, "ip", "netns", "list");
556     }
557
558 }