001package io.ebean.docker.commands;
002
003import io.ebean.docker.commands.process.ProcessResult;
004
005import java.io.File;
006import java.nio.file.Path;
007import java.sql.Connection;
008import java.sql.PreparedStatement;
009import java.sql.SQLException;
010import java.util.ArrayList;
011import java.util.List;
012import java.util.Properties;
013
014import static io.ebean.docker.commands.process.ProcessHandler.process;
015
016public class NuoDBContainer extends JdbcBaseDbContainer {
017
018  private static final String AD_RESET = "com.nuodb.nagent.AgentMain main Entering initializing for server";
019  private static final String AD_RUNNING = "com.nuodb.nagent.AgentMain main NuoAdmin Server running";
020  private static final String SM_RESET = "Starting Storage Manager";
021  private static final String SM_RUNNING = "Database formed";
022  private static final String SM_UNABLE_TO_CONNECT = "Unable to connect ";
023  private static final String TE_RESET = "Starting Transaction Engine";
024  private static final String TE_RUNNING = "Database entered";
025
026  /**
027   * Create NuoDB container with configuration from properties.
028   */
029  public static NuoDBContainer create(String version, Properties properties) {
030    return new NuoDBContainer(new NuoDBConfig(version, properties));
031  }
032
033  private final NuoDBConfig nuoConfig;
034  private final String network;
035  private final String adName;
036  private final String smName;
037  private final String teName;
038
039  public NuoDBContainer(NuoDBConfig config) {
040    super(config);
041    this.checkConnectivityUsingAdmin = true;
042    config.initDefaultSchema();
043    this.nuoConfig = config;
044    this.network = config.getNetwork();
045    this.adName = nuoConfig.containerName();
046    this.smName = adName + "_" + nuoConfig.getSm1();
047    this.teName = adName + "_" + nuoConfig.getTe1();
048  }
049
050  @Override
051  public void stopRemove() {
052    if (stopDatabase()) {
053      commands.removeContainers(teName, smName, adName);
054    }
055    if (networkExists()) {
056      removeNetwork();
057    }
058  }
059
060  private void removeNetwork() {
061    process(procNetworkRemove());
062  }
063
064  @Override
065  public void stopOnly() {
066    stopDatabase();
067  }
068
069  private boolean stopDatabase() {
070
071    //  nuocmd shutdown database --db-name testdb
072    List<String> args = new ArrayList<>();
073    args.add(config.docker);
074    args.add("exec");
075    args.add("-i");
076    args.add(adName);
077    args.add("nuocmd");
078    args.add("shutdown");
079    args.add("database");
080    args.add("--db-name");
081    args.add(dbConfig.getDbName());
082
083    final ProcessResult result = process(createProcessBuilder(args));
084    if (!result.success()) {
085      log.error("Error performing shutdown database " + result);
086      return false;
087    }
088    waitTime(100);
089    commands.stop(adName);
090    return true;
091  }
092
093  @Override
094  void runContainer() {
095    createNetwork();
096    process(runAdminProcess());
097    if (waitForAdminProcess()) {
098      process(runStorageManager());
099      if (waitForStorageManager()) {
100        process(runTransactionManager());
101        waitForTransactionManager();
102      }
103    }
104  }
105
106  private boolean waitForTransactionManager() {
107    return waitForLogs(teName, TE_RUNNING, TE_RESET) && waitTime(100);
108  }
109
110  private boolean storageManagerUnableToConnect() {
111
112    boolean unableToConnect = false;
113
114    final List<String> logs = commands.logs(smName);
115    for (String log : logs) {
116      if (log.contains(SM_UNABLE_TO_CONNECT)) {
117        unableToConnect = true;
118      } else if (log.contains(SM_RUNNING)) {
119        unableToConnect = false;
120      }
121    }
122    return unableToConnect;
123  }
124
125  private boolean waitForStorageManager() {
126    return waitForLogs(smName, SM_RUNNING, SM_RESET);
127  }
128
129  private boolean waitForAdminProcess() {
130    return waitForLogs(config.containerName(), AD_RUNNING, AD_RESET);
131  }
132
133  private boolean waitTime(long millis) {
134    try {
135      Thread.sleep(millis);
136    } catch (InterruptedException e) {
137      Thread.currentThread().interrupt();
138      e.printStackTrace();
139    }
140    return true;
141  }
142
143  private boolean waitForLogs(String containerName, String match, String resetMatch) {
144    for (int i = 0; i < 150; i++) {
145      if (logsContain(containerName, match, resetMatch)) {
146        return true;
147      }
148      try {
149        int sleep = (i < 10) ? 10 : (i < 20) ? 20 : 100;
150        Thread.sleep(sleep);
151      } catch (InterruptedException e) {
152        Thread.currentThread().interrupt();
153        return false;
154      }
155    }
156    return false;
157  }
158
159  @Override
160  void startContainer() {
161    if (!isArchivePopulated()) {
162      removeContainersAndRun();
163
164    } else {
165      commands.start(adName);
166      if (!waitForAdminProcess() || !waitForDatabaseState()) {
167        throw new RuntimeException("Failed waiting for NuoDB admin container [" + smName + "] to start running");
168      } else {
169        if (!startStorageManager(0)) {
170          throw new RuntimeException("Failed to start storage manager NuoDB [" + adName + "]");
171        } else {
172          commands.start(teName);
173          if (!waitForTransactionManager()) {
174            throw new RuntimeException("Failed waiting for NuoDB transaction manager [" + smName + "] to start running");
175          }
176        }
177      }
178    }
179  }
180
181  private void removeContainersAndRun() {
182    log.info("Archive directory is empty, remove containers and run");
183    commands.removeContainers(teName, smName, adName);
184    runContainer();
185  }
186
187  private boolean waitForDatabaseState() {
188    waitTime(100);
189    for (int i = 0; i < 20; i++) {
190      if (checkDbStateOk()) {
191        return true;
192      } else {
193        waitTime(100);
194      }
195    }
196    return false;
197  }
198
199  private boolean checkDbStateOk() {
200    //$ nuocmd show database  --db-format 'dbState:{state}'  --db-name testdb
201    List<String> args = new ArrayList<>();
202    args.add(config.docker);
203    args.add("exec");
204    args.add("-i");
205    args.add(adName);
206    args.add("nuocmd");
207    args.add("show");
208    args.add("database");
209    args.add("--db-format");
210    args.add("dbState:{state}");
211    args.add("--db-name");
212    args.add(dbConfig.getDbName());
213
214    try {
215      final ProcessResult result = process(createProcessBuilder(args));
216      if (result.success()) {
217        for (String outLine : result.getOutLines()) {
218          final String trimmedOut = outLine.trim();
219          if (trimmedOut.startsWith("dbState:")) {
220            return dbStateOk(trimmedOut);
221          }
222        }
223      }
224    } catch (CommandException e) {
225      return false;
226    }
227    return false;
228  }
229
230  private boolean dbStateOk(String trimmedOut) {
231    log.trace("checking dbStateOk [{}]", trimmedOut);
232    return trimmedOut.contains("NOT_RUNNING") || trimmedOut.contains("RUNNING");
233  }
234
235  private boolean startStorageManager(int attempt) {
236    commands.start(smName);
237    if (!waitForStorageManager()) {
238      log.error("Failed waiting for NuoDB storage manager [" + adName + "] to start running");
239      return false;
240    }
241    if (storageManagerUnableToConnect()) {
242      log.info("Retry NuoDB storage manager [" + adName + "] attempt:" + attempt);
243      return attempt <= 2 && startStorageManager(attempt + 1);
244    }
245    return true;
246  }
247
248  private void createNetwork() {
249    if (!networkExists()) {
250      process(procNetworkCreate());
251    }
252  }
253
254  private boolean networkExists() {
255    return execute(network, procNetworkList());
256  }
257
258  private ProcessBuilder procNetworkCreate() {
259
260    List<String> args = new ArrayList<>();
261    args.add(config.docker);
262    args.add("network");
263    args.add("create");
264    args.add(network);
265    return createProcessBuilder(args);
266  }
267
268  private ProcessBuilder procNetworkRemove() {
269
270    List<String> args = new ArrayList<>();
271    args.add(config.docker);
272    args.add("network");
273    args.add("rm");
274    args.add(network);
275    return createProcessBuilder(args);
276  }
277
278  private ProcessBuilder procNetworkList() {
279
280    List<String> args = new ArrayList<>();
281    args.add(config.docker);
282    args.add("network");
283    args.add("ls");
284    args.add("-f");
285    args.add("name=" + network);
286    return createProcessBuilder(args);
287  }
288
289  @Override
290  protected ProcessBuilder runProcess() {
291    throw new RuntimeException("Not used for NuoDB container");
292  }
293
294  private ProcessBuilder runAdminProcess() {
295
296    List<String> args = new ArrayList<>();
297    args.add(config.docker);
298    args.add("run");
299    args.add("-d");
300    args.add("--name");
301    args.add(adName);
302    args.add("--hostname");
303    args.add(adName);
304    args.add("--net");
305    args.add(network);
306    args.add("-p");
307    args.add(config.getPort() + ":" + config.getInternalPort());
308    args.add("-p");
309    args.add(nuoConfig.getPort2() + ":" + nuoConfig.getInternalPort2());
310    args.add("-p");
311    args.add(nuoConfig.getPort3() + ":" + nuoConfig.getInternalPort3());
312
313    if (defined(dbConfig.getAdminPassword())) {
314      args.add("-e");
315      args.add("NUODB_DOMAIN_ENTRYPOINT=" + adName);
316    }
317    args.add(config.getImage());
318    args.add("nuoadmin");
319    return createProcessBuilder(args);
320  }
321
322  private ProcessBuilder runStorageManager() {
323
324    // volumes for backup and archive not added yet
325    // as generally we are application testing with this
326
327    final Path archiveDir = archivePath();
328
329    List<String> args = new ArrayList<>();
330    args.add(config.docker);
331    args.add("run");
332    args.add("-d");
333    args.add("--name");
334    args.add(smName);
335    args.add("--hostname");
336    args.add(smName);
337    args.add("--volume");
338    args.add(archiveDir.toAbsolutePath().toString() + ":/var/opt/nuodb/archive");
339    args.add("--net");
340    args.add(network);
341    args.add(config.getImage());
342    args.add("nuodocker");
343    args.add("--api-server");
344    args.add(adName + ":" + config.getPort());
345    args.add("start");
346    args.add("sm");
347    args.add("--db-name");
348    args.add(dbConfig.getDbName());
349    args.add("--server-id");
350    args.add(adName);
351    args.add("--dba-user");
352    args.add(dbConfig.getAdminUsername());
353    args.add("--dba-password");
354    args.add(dbConfig.getAdminPassword());
355    args.add("--labels");
356    args.add(nuoConfig.getLabels());
357    args.add("--archive-dir");
358    args.add("/var/opt/nuodb/archive");
359
360    return createProcessBuilder(args);
361  }
362
363  boolean deleteDirectory(File dir) {
364    File[] allContents = dir.listFiles();
365    if (allContents != null) {
366      for (File file : allContents) {
367        deleteDirectory(file);
368      }
369    }
370    return dir.delete();
371  }
372
373  private Path archivePath() {
374    File nuoArchive = archiveFile();
375    if (nuoArchive.exists()) {
376      log.info("delete " + nuoArchive.toPath());
377      deleteDirectory(nuoArchive);
378    } else {
379      nuoArchive.setWritable(true, false);
380      if (!nuoArchive.mkdirs()) {
381        throw new RuntimeException("Failed to re-create " + nuoArchive.getAbsolutePath());
382      }
383    }
384    return nuoArchive.toPath();
385  }
386
387  private boolean isArchivePopulated() {
388    final File file = archiveFile();
389    if (file.exists()) {
390      final File[] files = file.listFiles();
391      return files != null && files.length > 0;
392    }
393    return false;
394  }
395
396  private File archiveFile() {
397    final File tmp = new File(System.getProperty("java.io.tmpdir"));
398    return new File(new File(tmp, "nuodb"), dbConfig.getDbName());
399  }
400
401  private ProcessBuilder runTransactionManager() {
402
403    List<String> args = new ArrayList<>();
404    args.add(config.docker);
405    args.add("run");
406    args.add("-d");
407    args.add("--name");
408    args.add(teName);
409    args.add("--hostname");
410    args.add(teName);
411    args.add("--net");
412    args.add(network);
413    args.add(config.getImage());
414    args.add("nuodocker");
415    args.add("--api-server");
416    args.add(adName + ":" + config.getPort());
417    args.add("start");
418    args.add("te");
419    args.add("--db-name");
420    args.add(dbConfig.getDbName());
421    args.add("--server-id");
422    args.add(adName);
423
424    return createProcessBuilder(args);
425  }
426
427  @Override
428  public boolean isDatabaseReady() {
429    return commands.logsContain(config.containerName(), "NuoAdmin Server running");
430  }
431
432  @Override
433  protected boolean isDatabaseAdminReady() {
434    return true;
435  }
436
437  @Override
438  void createDatabase() {
439    createSchemaAndUser(false);
440  }
441
442  @Override
443  void dropCreateDatabase() {
444    createSchemaAndUser(true);
445  }
446
447  private void createSchemaAndUser(boolean withDrop) {
448
449    try (Connection connection = config.createAdminConnection()) {
450
451      if (withDrop) {
452        sqlDropSchema(connection, dbConfig.getSchema());
453      }
454
455      final boolean schemaExists = sqlSchemaExists(connection, dbConfig.getSchema());
456      if (!schemaExists) {
457        sqlCreateSchema(connection, dbConfig.getSchema());
458      }
459
460      final boolean userExists = sqlUserExists(connection, dbConfig.getUsername());
461      if (!userExists) {
462        sqlCreateUser(connection, dbConfig.getUsername(), dbConfig.getPassword());
463      }
464      if (withDrop || !userExists) {
465        sqlUserGrants(connection, dbConfig.getSchema(), dbConfig.getUsername());
466      }
467      connection.commit();
468
469    } catch (SQLException e) {
470      throw new RuntimeException(e);
471    }
472  }
473
474  private void sqlDropSchema(Connection connection, String schema) throws SQLException {
475    exeSql(connection, "drop schema " + schema + " cascade if exists");
476  }
477
478  private void sqlUserGrants(Connection connection, String schema, String username) throws SQLException {
479    exeSql(connection, "grant create on schema " + schema + " to " + username);
480  }
481
482  private void sqlCreateSchema(Connection connection, String schema) throws SQLException {
483    exeSql(connection, "create schema " + schema);
484  }
485
486  private void sqlCreateUser(Connection connection, String username, String password) throws SQLException {
487    exeSql(connection, "create user " + username + " password '" + password + "'");
488  }
489
490  private boolean sqlSchemaExists(Connection connection, String schemaName) throws SQLException {
491    return sqlQueryMatch(connection, "select schema from system.schemas", schemaName);
492  }
493
494  private boolean sqlUserExists(Connection connection, String dbUser) throws SQLException {
495    return sqlQueryMatch(connection, "select username from system.users", dbUser);
496  }
497
498  private void exeSql(Connection connection, String sql) throws SQLException {
499    log.debug("exeSql {}", sql);
500    try (PreparedStatement st = connection.prepareStatement(sql)) {
501      st.execute();
502    }
503  }
504}