ee8cce67034a45f27f7000997317894961d183ee
[netconf.git] / opendaylight / netconf / tools / netconf-testtool / src / main / java / org / opendaylight / netconf / test / tool / Main.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, 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.netconf.test.tool;
10
11 import static com.google.common.base.Preconditions.checkArgument;
12 import static com.google.common.base.Preconditions.checkNotNull;
13
14 import ch.qos.logback.classic.Level;
15 import com.google.common.base.Charsets;
16 import com.google.common.base.Preconditions;
17 import com.google.common.collect.Lists;
18 import com.google.common.io.ByteStreams;
19 import com.google.common.io.CharStreams;
20 import com.google.common.io.Files;
21 import java.io.File;
22 import java.io.FileFilter;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.List;
29 import java.util.concurrent.TimeUnit;
30 import net.sourceforge.argparse4j.ArgumentParsers;
31 import net.sourceforge.argparse4j.annotation.Arg;
32 import net.sourceforge.argparse4j.inf.ArgumentParser;
33 import net.sourceforge.argparse4j.inf.ArgumentParserException;
34 import org.opendaylight.controller.config.util.xml.XmlElement;
35 import org.opendaylight.controller.config.util.xml.XmlUtil;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.w3c.dom.Document;
39 import org.w3c.dom.Element;
40 import org.w3c.dom.Node;
41 import org.w3c.dom.NodeList;
42 import org.xml.sax.SAXException;
43
44 public final class Main {
45
46     private static final Logger LOG = LoggerFactory.getLogger(Main.class);
47
48     public static class Params {
49
50         @Arg(dest = "schemas-dir")
51         public File schemasDir;
52
53         @Arg(dest = "devices-count")
54         public int deviceCount;
55
56         @Arg(dest = "starting-port")
57         public int startingPort;
58
59         @Arg(dest = "generate-config-connection-timeout")
60         public int generateConfigsTimeout;
61
62         @Arg(dest = "generate-config-address")
63         public String generateConfigsAddress;
64
65         @Arg(dest = "distro-folder")
66         public File distroFolder;
67
68         @Arg(dest = "generate-configs-batch-size")
69         public int generateConfigBatchSize;
70
71         @Arg(dest = "ssh")
72         public boolean ssh;
73
74         @Arg(dest = "exi")
75         public boolean exi;
76
77         @Arg(dest = "debug")
78         public boolean debug;
79
80         @Arg(dest = "notification-file")
81         public File notificationFile;
82
83         @Arg(dest = "md-sal")
84         public boolean mdSal;
85
86         @Arg(dest = "initial-config-xml-file")
87         public File initialConfigXMLFile;
88
89         static ArgumentParser getParser() {
90             final ArgumentParser parser = ArgumentParsers.newArgumentParser("netconf testool");
91
92             parser.description("Netconf device simulator. Detailed info can be found at https://wiki.opendaylight.org/view/OpenDaylight_Controller:Netconf:Testtool#Building_testtool");
93
94             parser.addArgument("--device-count")
95                     .type(Integer.class)
96                     .setDefault(1)
97                     .type(Integer.class)
98                     .help("Number of simulated netconf devices to spin")
99                     .dest("devices-count");
100
101             parser.addArgument("--schemas-dir")
102                     .type(File.class)
103                     .help("Directory containing yang schemas to describe simulated devices. Some schemas e.g. netconf monitoring and inet types are included by default")
104                     .dest("schemas-dir");
105
106             parser.addArgument("--notification-file")
107                     .type(File.class)
108                     .help("Xml file containing notifications that should be sent to clients after create subscription is called")
109                     .dest("notification-file");
110
111             parser.addArgument("--initial-config-xml-file")
112                     .type(File.class)
113                     .help("Xml file containing initial simulatted configuration to be returned via get-config rpc")
114                     .dest("initial-config-xml-file");
115
116             parser.addArgument("--starting-port")
117                     .type(Integer.class)
118                     .setDefault(17830)
119                     .help("First port for simulated device. Each other device will have previous+1 port number")
120                     .dest("starting-port");
121
122             parser.addArgument("--generate-config-connection-timeout")
123                     .type(Integer.class)
124                     .setDefault((int)TimeUnit.MINUTES.toMillis(30))
125                     .help("Timeout to be generated in initial config files")
126                     .dest("generate-config-connection-timeout");
127
128             parser.addArgument("--generate-config-address")
129                     .type(String.class)
130                     .setDefault("127.0.0.1")
131                     .help("Address to be placed in generated configs")
132                     .dest("generate-config-address");
133
134             parser.addArgument("--generate-configs-batch-size")
135                     .type(Integer.class)
136                     .setDefault(4000)
137                     .help("Number of connector configs per generated file")
138                     .dest("generate-configs-batch-size");
139
140             parser.addArgument("--distribution-folder")
141                     .type(File.class)
142                     .help("Directory where the karaf distribution for controller is located")
143                     .dest("distro-folder");
144
145             parser.addArgument("--ssh")
146                     .type(Boolean.class)
147                     .setDefault(true)
148                     .help("Whether to use ssh for transport or just pure tcp")
149                     .dest("ssh");
150
151             parser.addArgument("--exi")
152                     .type(Boolean.class)
153                     .setDefault(true)
154                     .help("Whether to use exi to transport xml content")
155                     .dest("exi");
156
157             parser.addArgument("--debug")
158                     .type(Boolean.class)
159                     .setDefault(false)
160                     .help("Whether to use debug log level instead of INFO")
161                     .dest("debug");
162
163             parser.addArgument("--md-sal")
164                     .type(Boolean.class)
165                     .setDefault(false)
166                     .help("Whether to use md-sal datastore instead of default simulated datastore.")
167                     .dest("md-sal");
168
169             return parser;
170         }
171
172         void validate() {
173             checkArgument(deviceCount > 0, "Device count has to be > 0");
174             checkArgument(startingPort > 1023, "Starting port has to be > 1023");
175
176             if(schemasDir != null) {
177                 checkArgument(schemasDir.exists(), "Schemas dir has to exist");
178                 checkArgument(schemasDir.isDirectory(), "Schemas dir has to be a directory");
179                 checkArgument(schemasDir.canRead(), "Schemas dir has to be readable");
180             }
181         }
182     }
183
184     public static void main(final String[] args) {
185         final Params params = parseArgs(args, Params.getParser());
186         params.validate();
187
188         final ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
189         root.setLevel(params.debug ? Level.DEBUG : Level.INFO);
190
191         final NetconfDeviceSimulator netconfDeviceSimulator = new NetconfDeviceSimulator();
192         try {
193             final List<Integer> openDevices = netconfDeviceSimulator.start(params);
194             if (openDevices.size() == 0) {
195                 LOG.error("Failed to start any simulated devices, exiting...");
196                 System.exit(1);
197             }
198             if(params.distroFolder != null) {
199                 final ConfigGenerator configGenerator = new ConfigGenerator(params.distroFolder, openDevices);
200                 final List<File> generated = configGenerator.generate(params.ssh, params.generateConfigBatchSize, params.generateConfigsTimeout, params.generateConfigsAddress);
201                 configGenerator.updateFeatureFile(generated);
202                 configGenerator.changeLoadOrder();
203             }
204         } catch (final Exception e) {
205             LOG.error("Unhandled exception", e);
206             netconfDeviceSimulator.close();
207             System.exit(1);
208         }
209
210         // Block main thread
211         synchronized (netconfDeviceSimulator) {
212             try {
213                 netconfDeviceSimulator.wait();
214             } catch (final InterruptedException e) {
215                 throw new RuntimeException(e);
216             }
217         }
218     }
219
220     private static Params parseArgs(final String[] args, final ArgumentParser parser) {
221         final Params opt = new Params();
222         try {
223             parser.parseArgs(args, opt);
224             return opt;
225         } catch (final ArgumentParserException e) {
226             parser.handleError(e);
227         }
228
229         System.exit(1);
230         return null;
231     }
232
233     private static class ConfigGenerator {
234         public static final String NETCONF_CONNECTOR_XML = "/99-netconf-connector-simulated.xml";
235         public static final String SIM_DEVICE_SUFFIX = "-sim-device";
236
237         private static final String SIM_DEVICE_CFG_PREFIX = "simulated-devices_";
238         private static final String ETC_KARAF_PATH = "etc/";
239         private static final String ETC_OPENDAYLIGHT_KARAF_PATH = ETC_KARAF_PATH + "opendaylight/karaf/";
240
241         public static final String NETCONF_CONNECTOR_ALL_FEATURE = "odl-netconf-connector-all";
242         private static final String ORG_OPS4J_PAX_URL_MVN_CFG = "org.ops4j.pax.url.mvn.cfg";
243
244         private final File configDir;
245         private final List<Integer> openDevices;
246         private final List<File> ncFeatureFiles;
247         private final File etcDir;
248         private final File loadOrderCfgFile;
249
250         public ConfigGenerator(final File directory, final List<Integer> openDevices) {
251             this.configDir = new File(directory, ETC_OPENDAYLIGHT_KARAF_PATH);
252             this.etcDir = new File(directory, ETC_KARAF_PATH);
253             this.loadOrderCfgFile = new File(etcDir, ORG_OPS4J_PAX_URL_MVN_CFG);
254             this.ncFeatureFiles = getFeatureFile(directory, "features-netconf-connector", "xml");
255             this.openDevices = openDevices;
256         }
257
258         public List<File> generate(final boolean useSsh, final int batchSize, final int generateConfigsTimeout, final String address) {
259             if(configDir.exists() == false) {
260                 Preconditions.checkState(configDir.mkdirs(), "Unable to create directory " + configDir);
261             }
262
263             for (final File file : configDir.listFiles(new FileFilter() {
264                 @Override
265                 public boolean accept(final File pathname) {
266                     return !pathname.isDirectory() && pathname.getName().startsWith(SIM_DEVICE_CFG_PREFIX);
267                 }
268             })) {
269                 Preconditions.checkState(file.delete(), "Unable to clean previous generated file %s", file);
270             }
271
272             try(InputStream stream = Main.class.getResourceAsStream(NETCONF_CONNECTOR_XML)) {
273                 checkNotNull(stream, "Cannot load %s", NETCONF_CONNECTOR_XML);
274                 String configBlueprint = CharStreams.toString(new InputStreamReader(stream, Charsets.UTF_8));
275
276                 final String before = configBlueprint.substring(0, configBlueprint.indexOf("<module>"));
277                 final String middleBlueprint = configBlueprint.substring(configBlueprint.indexOf("<module>"), configBlueprint.indexOf("</module>"));
278                 final String after = configBlueprint.substring(configBlueprint.indexOf("</module>") + "</module>".length());
279
280                 int connectorCount = 0;
281                 Integer batchStart = null;
282                 StringBuilder b = new StringBuilder();
283                 b.append(before);
284
285                 final List<File> generatedConfigs = Lists.newArrayList();
286
287                 for (final Integer openDevice : openDevices) {
288                     if(batchStart == null) {
289                         batchStart = openDevice;
290                     }
291
292                     final String name = String.valueOf(openDevice) + SIM_DEVICE_SUFFIX;
293                     String configContent = String.format(middleBlueprint, name, address, String.valueOf(openDevice), String.valueOf(!useSsh));
294                     configContent = String.format("%s%s%d%s\n%s\n", configContent, "<connection-timeout-millis>", generateConfigsTimeout, "</connection-timeout-millis>", "</module>");
295
296                     b.append(configContent);
297                     connectorCount++;
298                     if(connectorCount == batchSize) {
299                         b.append(after);
300                         final File to = new File(configDir, String.format(SIM_DEVICE_CFG_PREFIX + "%d-%d.xml", batchStart, openDevice));
301                         generatedConfigs.add(to);
302                         Files.write(b.toString(), to, Charsets.UTF_8);
303                         connectorCount = 0;
304                         b = new StringBuilder();
305                         b.append(before);
306                         batchStart = null;
307                     }
308                 }
309
310                 // Write remaining
311                 if(connectorCount != 0) {
312                     b.append(after);
313                     final File to = new File(configDir, String.format(SIM_DEVICE_CFG_PREFIX + "%d-%d.xml", batchStart, openDevices.get(openDevices.size() - 1)));
314                     generatedConfigs.add(to);
315                     Files.write(b.toString(), to, Charsets.UTF_8);
316                 }
317
318                 LOG.info("Config files generated in {}", configDir);
319                 return generatedConfigs;
320             } catch (final IOException e) {
321                 throw new RuntimeException("Unable to generate config files", e);
322             }
323         }
324
325
326         public void updateFeatureFile(final List<File> generated) {
327             // TODO karaf core contains jaxb for feature files, use that for
328             // modification
329             try {
330                 for (final File featureFile : ncFeatureFiles) {
331                     final Document document = XmlUtil.readXmlToDocument(Files
332                             .toString(featureFile, Charsets.UTF_8));
333                     final NodeList childNodes = document.getDocumentElement().getChildNodes();
334
335                     for (int i = 0; i < childNodes.getLength(); i++) {
336                         final Node item = childNodes.item(i);
337                         if (item instanceof Element == false) {
338                             continue;
339                         }
340                         if (item.getLocalName().equals("feature") == false) {
341                             continue;
342                         }
343
344                         if (NETCONF_CONNECTOR_ALL_FEATURE
345                                 .equals(((Element) item).getAttribute("name"))) {
346                             final Element ncAllFeatureDefinition = (Element) item;
347                             // Clean previous generated files
348                             for (final XmlElement configfile : XmlElement
349                                     .fromDomElement(ncAllFeatureDefinition)
350                                     .getChildElements("configfile")) {
351                                 ncAllFeatureDefinition.removeChild(configfile.getDomElement());
352                             }
353                             for (final File file : generated) {
354                                 final Element configfile = document.createElement("configfile");
355                                 configfile.setTextContent("file:"
356                                         + ETC_OPENDAYLIGHT_KARAF_PATH
357                                         + file.getName());
358                                 configfile.setAttribute(
359                                         "finalname",
360                                         ETC_OPENDAYLIGHT_KARAF_PATH
361                                                 + file.getName());
362                                 ncAllFeatureDefinition.appendChild(configfile);
363                             }
364                         }
365                     }
366
367                     Files.write(XmlUtil.toString(document), featureFile,Charsets.UTF_8);
368                     LOG.info("Feature file {} updated", featureFile);
369                 }
370             } catch (final IOException e) {
371                 throw new RuntimeException("Unable to load features file as a resource");
372             } catch (final SAXException e) {
373                 throw new RuntimeException("Unable to parse features file");
374             }
375         }
376
377
378         private static List<File> getFeatureFile(final File distroFolder, final String featureName, final String suffix) {
379             checkExistingDir(distroFolder, String.format("Folder %s does not exist", distroFolder));
380
381             final File systemDir = checkExistingDir(new File(distroFolder, "system"), String.format("Folder %s does not contain a karaf distro, folder system is missing", distroFolder));
382
383             //check if beryllium path exists, if it doesnt check for lithium and fail/succeed after
384             File netconfConnectorFeaturesParentDir = new File(systemDir, "org/opendaylight/netconf/" + featureName);
385             if (!netconfConnectorFeaturesParentDir.exists() || !netconfConnectorFeaturesParentDir.isDirectory()) {
386                 netconfConnectorFeaturesParentDir = checkExistingDir(new File(systemDir, "org/opendaylight/controller/" + featureName), String.format("Karaf distro in %s does not contain netconf-connector features", distroFolder));
387             }
388
389             // Find newest version for features
390             final File newestVersionDir = Collections.max(
391                     Lists.newArrayList(netconfConnectorFeaturesParentDir.listFiles(new FileFilter() {
392                         @Override
393                         public boolean accept(final File pathname) {
394                             return pathname.isDirectory();
395                         }
396                     })), new Comparator<File>() {
397                         @Override
398                         public int compare(final File o1, final File o2) {
399                             return o1.getName().compareTo(o2.getName());
400                         }
401                     });
402
403             return Lists.newArrayList(newestVersionDir.listFiles(new FileFilter() {
404                 @Override
405                 public boolean accept(final File pathname) {
406                     return pathname.getName().contains(featureName)
407                             && Files.getFileExtension(pathname.getName()).equals(suffix);
408                 }
409             }));
410         }
411
412         private static File checkExistingDir(final File folder, final String msg) {
413             Preconditions.checkArgument(folder.exists(), msg);
414             Preconditions.checkArgument(folder.isDirectory(), msg);
415             return folder;
416         }
417
418         public void changeLoadOrder() {
419             try {
420                 Files.write(ByteStreams.toByteArray(getClass().getResourceAsStream("/" +ORG_OPS4J_PAX_URL_MVN_CFG)), loadOrderCfgFile);
421                 LOG.info("Load order changed to prefer local bundles/features by rewriting file {}", loadOrderCfgFile);
422             } catch (IOException e) {
423                 throw new RuntimeException("Unable to rewrite features file " + loadOrderCfgFile, e);
424             }
425         }
426     }
427 }