Merge "checkstyle fixes for library"
[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 = "docker-ovs-2.5.1.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
126     class DockerComposeServiceInfo {
127         public String name;
128         public String port;
129     }
130     private List<DockerComposeServiceInfo> dockerComposeServices = new ArrayList<DockerComposeServiceInfo>();
131
132     /**
133      * Get the array of system properties as pax exam Option objects for use in pax exam
134      * unit tests with Configuration annotation.
135      * @return List of Option objects
136      */
137     public static Option[] getSysPropOptions() {
138         return new Option[] {
139                 propagateSystemProperties(ItConstants.SERVER_IPADDRESS,
140                                             ItConstants.SERVER_PORT,
141                                             ItConstants.CONNECTION_TYPE,
142                                             ItConstants.CONTROLLER_IPADDRESS,
143                                             ItConstants.USERSPACE_ENABLED,
144                                             ItConstants.DOCKER_COMPOSE_FILE_NAME,
145                                             ItConstants.DOCKER_RUN)
146         };
147     }
148
149     /**
150      * Bring up all docker images in the default docker-compose file.
151      * @throws IOException if something goes wrong on the IO end
152      * @throws InterruptedException If this thread is interrupted
153      */
154     public DockerOvs() throws IOException, InterruptedException {
155         this(DEFAULT_DOCKER_FILE);
156     }
157
158     /**
159      * Bring up all docker images in the provided docker-compose file under "META-INF/docker-compose-files/".
160      * @param yamlFileName Just the file name
161      * @throws IOException if something goes wrong on the IO end
162      * @throws InterruptedException If this thread is interrupted
163      */
164     public DockerOvs(String yamlFileName) throws IOException, InterruptedException {
165         configureFromEnv();
166
167         if (!runDocker) {
168             LOG.info("DockerOvs.DockerOvs: Not running docker, -D{} specified", ItConstants.SERVER_IPADDRESS);
169             return;
170         }
171
172         tmpDockerComposeFile = createTempDockerComposeFile(yamlFileName);
173         buildDockerComposeCommands();
174         parseDockerComposeYaml();
175
176         isRunning = false;
177         //We run this for A LONG TIME since on the first run docker must download the
178         //image from docker hub. In experience it takes significantly less than this
179         //even when downloading the image. Once the image is downloaded this command
180         //runs like that <snaps fingers>
181         ProcUtils.runProcess(60000, upCmd);
182         isRunning = true;
183         waitForOvsdbServers(10 * 1000);
184     }
185
186     /**
187      * Pull required configuration from System.getProperties() and validate we have what we need.
188      * Note: Note that there is some minor complexity in how this class is configured using System
189      * properties. This stems from the fact that we want to preserve the meaning of these properties
190      * prior to the introduction of this class. See the ENV_USAGE variable for details.
191      */
192     private void configureFromEnv() {
193         Properties env = System.getProperties();
194         envServerAddress = env.getProperty(ItConstants.SERVER_IPADDRESS);
195         envServerPort = env.getProperty(ItConstants.SERVER_PORT);
196         String envRunDocker = env.getProperty(ItConstants.DOCKER_RUN);
197         String connType = env.getProperty(ItConstants.CONNECTION_TYPE, ItConstants.CONNECTION_TYPE_ACTIVE);
198         String dockerFile = env.getProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME);
199         envDockerComposeFile = DOCKER_FILE_PATH + (null == dockerFile ? DEFAULT_DOCKER_FILE : dockerFile);
200
201         //Are we running docker? If we specified docker.run, that's the answer. Otherwise, if there is a server
202         //address we assume docker is already running
203         runDocker = (envRunDocker != null) ? Boolean.parseBoolean(envRunDocker) : envServerAddress == null;
204
205         if(runDocker) {
206             return;
207         }
208
209         if (connType.equals(ItConstants.CONNECTION_TYPE_PASSIVE)) {
210             return;
211         }
212
213         //At this point we know we're not running docker and the conn type is active - make sure we have what we need
214         //If we have a server address than we require a port too as those
215         //are returned in getOvsdbPort() and getOvsdbAddress()
216         Assert.assertNotNull("Attempt to connect to previous running ovs but missing -Dovsdbserver.ipaddress\n"
217                                                                                     + ENV_USAGE, envServerAddress);
218         Assert.assertNotNull("Attempt to connect to previous running ovs but missing -Dovsdbserver.port\n"
219                 + ENV_USAGE, envServerPort);
220     }
221
222     /**
223      * Verify and build the docker-compose commands we will be running. This function adds the docker-compose file
224      * to the command lines and also checks (and adjusts the command line) as to whether sudo is required. This is
225      * done by attempting to run "docker-compose ps" without and then with sudo
226      * @throws IOException if something goes wrong on the IO end
227      * @throws InterruptedException If this thread is interrupted
228      */
229     private void buildDockerComposeCommands() throws IOException, InterruptedException {
230         psCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
231         psCmdNoSudo[COMPOSE_FILE_IDX_NO_SUDO] = tmpDockerComposeFile.toString();
232         upCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
233         downCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
234         execCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
235
236         if (0 == ProcUtils.tryProcess(5000, psCmdNoSudo)) {
237             LOG.info("DockerOvs.buildDockerComposeCommands docker-compose does not require sudo");
238             String[] tmp;
239             tmp = Arrays.copyOfRange(upCmd, 1, upCmd.length);
240             upCmd = tmp;
241             tmp = Arrays.copyOfRange(downCmd, 1, downCmd.length);
242             downCmd = tmp;
243             tmp = Arrays.copyOfRange(execCmd, 1, execCmd.length);
244             execCmd = tmp;
245         } else if (0 == ProcUtils.tryProcess(5000, psCmd)) {
246             LOG.info("DockerOvs.buildDockerComposeCommands docker-compose requires sudo");
247         } else {
248             Assert.fail("docker-compose does not seem to work with or without sudo");
249         }
250     }
251
252     /**
253      * Are we using some other OVS, not a docker we spin up?
254      * @return true if we are *not* running a docker image to test against
255      */
256     public boolean usingExternalDocker() {
257         return !runDocker;
258     }
259
260     /**
261      * Get the IP address of the n'th OVS.
262      * @param ovsNumber which OVS?
263      * @return IP string
264      */
265     public String getOvsdbAddress(int ovsNumber) {
266         if (!runDocker) {
267             return envServerAddress;
268         }
269         return DEFAULT_OVSDB_HOST;
270     }
271
272     /**
273      * Get the port of the n'th OVS.
274      * @param ovsNumber which OVS?
275      * @return Port as a string
276      */
277     public String getOvsdbPort(int ovsNumber) {
278         if (!runDocker) {
279             return envServerPort;
280         }
281         return dockerComposeServices.get(ovsNumber).port;
282     }
283
284     /**
285      * How many OVS nodes are there.
286      * @return number of running OVS nodes
287      */
288     public int getNumOvsNodes() {
289         return dockerComposeServices.size();
290     }
291
292     public String[] getExecCmdPrefix(int numOvs) {
293         String[] res = new String[execCmd.length];
294         System.arraycopy(execCmd, 0, res, 0, execCmd.length);
295         res[res.length - 1] = dockerComposeServices.get(numOvs).name;
296         return res;
297     }
298
299     public void runInContainer(int waitFor, int numOvs, String ... cmdWords) throws IOException, InterruptedException {
300         String[] pfx = getExecCmdPrefix(numOvs);
301         String[] cmd = new String[pfx.length + cmdWords.length];
302         System.arraycopy(pfx, 0, cmd, 0, pfx.length);
303         System.arraycopy(cmdWords, 0, cmd, pfx.length, cmdWords.length);
304         ProcUtils.runProcess(waitFor, cmd);
305     }
306
307     public void tryInContainer(int waitFor, int numOvs, String ... cmdWords) throws IOException, InterruptedException {
308         String[] pfx = getExecCmdPrefix(numOvs);
309         String[] cmd = new String[pfx.length + cmdWords.length];
310         System.arraycopy(pfx, 0, cmd, 0, pfx.length);
311         System.arraycopy(cmdWords, 0, cmd, pfx.length, cmdWords.length);
312         ProcUtils.tryProcess(waitFor, cmd);
313     }
314
315     /**
316      * Parse the docker-compose yaml file to extract the port mappings.
317      * @return a list of the external ports
318      */
319     private List<String> parseDockerComposeYaml() {
320         List<String> ports = new ArrayList<String>();
321
322         YamlReader yamlReader = null;
323         Map root = null;
324         try {
325             yamlReader = new YamlReader(new FileReader(tmpDockerComposeFile));
326             root = (Map) yamlReader.read();
327         } catch (FileNotFoundException e) {
328             LOG.warn("DockerOvs.parseDockerComposeYaml error reading yaml file", e);
329             return ports;
330         } catch (YamlException e) {
331             LOG.warn("DockerOvs.parseDockerComposeYaml error parsing yaml file", e);
332             return ports;
333         }
334
335         if (null == root) {
336             return ports;
337         }
338         for (Object entry : root.entrySet()) {
339             String key = ((Map.Entry<String,Map>)entry).getKey();
340             Map map = ((Map.Entry<String,Map>)entry).getValue();
341
342             DockerComposeServiceInfo svc = new DockerComposeServiceInfo();
343             svc.name = key;
344
345             List portMappings = (List) map.get("ports");
346             if (null == portMappings) {
347                 continue;
348             }
349             for (Object portMapping : portMappings) {
350                 String portMappingStr = (String) portMapping;
351                 int delim = portMappingStr.indexOf(":");
352                 if (delim == -1) {
353                     continue;
354                 }
355                 String port = portMappingStr.substring(0, delim);
356                 ports.add(port);
357                 svc.port = port;
358             }
359             //TODO: think this through. What if there is no port?
360             dockerComposeServices.add(svc);
361         }
362
363         return ports;
364     }
365
366     /**
367      * Shut everything down.
368      * @throws Exception but not really
369      */
370     @Override
371     public void close() throws Exception {
372         if (isRunning) {
373             ProcUtils.runProcess(10000, downCmd);
374             isRunning = false;
375         }
376
377         try {
378             tmpDockerComposeFile.delete();
379         } catch (Exception ignored) {
380             //No reason to fail the test, we're just being polite here.
381         }
382     }
383
384     /**
385      * A thread that waits until it can "ping" a running OVS -  tests basic reachability
386      * and readiness. The "ping" here is actually a list_dbs method and the response is
387      * checked to make sure the Open_Vswitch DB is present. Note that this thread will
388      * run until it succeeds unless its interrupt() method is called.
389      */
390     class OvsdbPing extends Thread {
391
392         private final String host;
393         private final int port;
394         private final AtomicInteger result;
395         public CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
396         ByteBuffer listDbsRequest;
397
398         /**
399          * Construct a new OvsdbPing object.
400          * @param ovsNumber which OVS is this?
401          * @param result an AtomicInteger that is incremented upon a successful "ping"
402          */
403         public OvsdbPing(int ovsNumber, AtomicInteger result) {
404             this.host = getOvsdbAddress(ovsNumber);
405             this.port = Integer.parseInt(getOvsdbPort(ovsNumber));
406             this.result = result;
407             listDbsRequest = ByteBuffer.wrap(
408                     ("{\"method\": \"list_dbs\", \"params\": [], \"id\": " + port + "}").getBytes());
409             listDbsRequest.mark();
410         }
411
412         @Override
413         public void run() {
414             while (!doPing()) {
415                 try {
416                     Thread.sleep(1000);
417                 } catch (InterruptedException e) {
418                     LOG.warn("OvsdbPing interrupted", e);
419                     return;
420                 }
421             }
422         }
423
424         /**
425          * Attempt a "ping" of the OVSDB connection.
426          * @return true if the ping was successful OR IF THIS THREAD WAS INTERRUPTED
427          */
428         private boolean doPing() {
429             try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port))) {
430                 socketChannel.write(listDbsRequest);
431                 listDbsRequest.reset();
432
433                 ByteBuffer buf = ByteBuffer.allocateDirect(512);
434                 socketChannel.read(buf);
435                 buf.flip();
436                 String response = decoder.decode(buf).toString();
437
438                 if (response.contains("Open_vSwitch")) {
439                     LOG.info("OvsdbPing connection validated");
440                     result.incrementAndGet();
441                     return true;
442                 }
443             } catch (ClosedByInterruptException e) {
444                 LOG.warn("OvsdbPing interrupted", e);
445                 //return true here because we're done, ne'er to return again.
446                 return true;
447             } catch (Exception e) {
448                 LOG.info("OvsdbPing exception while attempting connect {}", e.toString());
449             }
450             return false;
451         }
452     }
453
454     /**
455      * Wait for all Ovs's to accept and respond to OVSDB requests.
456      * @param waitFor How long to wait
457      * @throws IOException if something goes wrong on the IO end
458      * @throws InterruptedException If this thread is interrupted
459      */
460     private void waitForOvsdbServers(long waitFor) throws IOException, InterruptedException {
461         AtomicInteger numRunningOvs = new AtomicInteger(0);
462
463         int numOvs = dockerComposeServices.size();
464         if (0 == numOvs) {
465             return;
466         }
467
468         OvsdbPing[] pingers = new OvsdbPing[numOvs];
469         for (int i = 0; i < numOvs; i++) {
470             pingers[i] = new OvsdbPing(i, numRunningOvs);
471             pingers[i].start();
472         }
473
474         long startTime = System.currentTimeMillis();
475         while ( (System.currentTimeMillis() - startTime) < waitFor) {
476             if (numRunningOvs.get() >= numOvs) {
477                 LOG.info("DockerOvs.waitForOvsdbServers all OVS instances running");
478                 break;
479             }
480             Thread.sleep(1000);
481         }
482         LOG.info("DockerOvs.waitForOvsdbServers - finished waiting in {}", System.currentTimeMillis() - startTime);
483
484         for (OvsdbPing pinger : pingers) {
485             pinger.interrupt();
486         }
487     }
488
489     /**
490      * Since the docker-compose file is a resource in the bundle and docker-compose needs it.
491      * in the file system, we copy it over - ugly but necessary.
492      * @param yamlFileName File name
493      * @return A File object for the newly created temporary yaml file.
494      */
495     private File createTempDockerComposeFile(String yamlFileName) {
496         Bundle bundle = FrameworkUtil.getBundle(this.getClass());
497         Assert.assertNotNull("DockerOvs: bundle is null", bundle);
498         URL url = bundle.getResource(envDockerComposeFile);
499         Assert.assertNotNull("DockerOvs: URL is null", url);
500
501         File tmpFile = null;
502         try {
503             tmpFile = File.createTempFile("ovsdb-it-tmp-", null);
504
505             try (Reader in = new InputStreamReader(url.openStream());
506                                 FileWriter out = new FileWriter(tmpFile)) {
507                 char[] buf = new char[1024];
508                 int read;
509                 while (-1 != (read = in.read(buf))) {
510                     out.write(buf, 0, read);
511                 }
512             }
513
514         } catch (IOException e) {
515             Assert.fail(e.toString());
516         }
517
518         return tmpFile;
519     }
520
521     /**
522      * Useful for debugging. Dump some interesting config
523      * @throws IOException If something goes wrong with reading the process output
524      * @throws InterruptedException because there's some sleeping in here
525      */
526     public void logState(int dockerInstance) throws IOException, InterruptedException {
527         tryInContainer(5000, dockerInstance, "ip", "addr");
528         tryInContainer(5000, dockerInstance, "ovs-vsctl", "show");
529         tryInContainer(5000, dockerInstance, "ovs-ofctl", "-OOpenFlow13", "show", "br-int");
530         tryInContainer(5000, dockerInstance, "ovs-ofctl", "-OOpenFlow13", "dump-flows", "br-int");
531         tryInContainer(5000, dockerInstance, "ip", "netns", "list");
532     }
533
534 }