@Override
  public void redeployAll() {
    Map<String, String> wars = MutableMap.copyOf(getConfig(WARS_BY_CONTEXT));
    String redeployPrefix = "Redeploy all WARs (count " + wars.size() + ")";

    log.debug("Redeplying all WARs across cluster " + this + ": " + getConfig(WARS_BY_CONTEXT));

    Iterable<CanDeployAndUndeploy> targetEntities =
        Iterables.filter(getChildren(), CanDeployAndUndeploy.class);
    TaskBuilder<Void> tb =
        Tasks.<Void>builder()
            .parallel(true)
            .name(redeployPrefix + " across cluster (size " + Iterables.size(targetEntities) + ")");
    for (Entity targetEntity : targetEntities) {
      TaskBuilder<Void> redeployAllToTarget =
          Tasks.<Void>builder()
              .name(redeployPrefix + " at " + targetEntity + " (after ready check)");
      for (String warContextPath : wars.keySet()) {
        redeployAllToTarget.add(
            Effectors.invocation(
                targetEntity,
                DEPLOY,
                MutableMap.of("url", wars.get(warContextPath), "targetName", warContextPath)));
      }
      tb.add(
          whenServiceUp(
              targetEntity,
              redeployAllToTarget.build(),
              redeployPrefix + " at " + targetEntity + " when ready"));
    }
    DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked();
  }
  @Override
  public void undeploy(String targetName) {
    checkNotNull(targetName, "targetName");
    targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName);

    // set it up so future nodes get the right wars
    if (!removeFromWarsByContext(this, targetName)) {
      DynamicTasks.submit(
          Tasks.warning(
              "Context "
                  + targetName
                  + " not known at "
                  + this
                  + "; attempting to undeploy regardless",
              null),
          this);
    }

    log.debug(
        "Undeploying "
            + targetName
            + " across cluster "
            + this
            + "; WARs now "
            + getConfig(WARS_BY_CONTEXT));

    Iterable<CanDeployAndUndeploy> targets =
        Iterables.filter(getChildren(), CanDeployAndUndeploy.class);
    TaskBuilder<Void> tb =
        Tasks.<Void>builder()
            .parallel(true)
            .name(
                "Undeploy "
                    + targetName
                    + " across cluster (size "
                    + Iterables.size(targets)
                    + ")");
    for (Entity target : targets) {
      tb.add(
          whenServiceUp(
              target,
              Effectors.invocation(target, UNDEPLOY, MutableMap.of("targetName", targetName)),
              "Undeploy " + targetName + " at " + target + " when ready"));
    }
    DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked();

    // Update attribute
    Set<String> deployedWars = MutableSet.copyOf(getAttribute(DEPLOYED_WARS));
    deployedWars.remove(
        FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName));
    setAttribute(DEPLOYED_WARS, deployedWars);
  }
 @Test
 public void testComplex() throws InterruptedException, ExecutionException {
   Task<List<?>> t =
       Tasks.sequential(
           sayTask("1"), sayTask("2"), Tasks.parallel(sayTask("4"), sayTask("3")), sayTask("5"));
   ec.submit(t);
   Assert.assertEquals(t.get().size(), 4);
   Asserts.assertEqualsIgnoringOrder((List<?>) t.get().get(2), ImmutableSet.of("3", "4"));
   Assert.assertTrue(
       messages.equals(Arrays.asList("1", "2", "3", "4", "5"))
           || messages.equals(Arrays.asList("1", "2", "4", "3", "5")),
       "messages=" + messages);
 }
 @Test
 public void testInessentialChildrenFailureDoesNotAbortSecondaryOrFailPrimary() {
   Task<String> t1 = monitorableTask(null, "1", new FailCallable());
   TaskTags.markInessential(t1);
   Task<String> t =
       Tasks.<String>builder()
           .dynamic(true)
           .body(monitorableJob("main"))
           .add(t1)
           .add(monitorableTask("2"))
           .build();
   ec.submit(t);
   releaseAndWaitForMonitorableJob("1");
   Assert.assertFalse(t.blockUntilEnded(TINY_TIME));
   releaseAndWaitForMonitorableJob("2");
   Assert.assertFalse(t.blockUntilEnded(TINY_TIME));
   releaseMonitorableJob("main");
   Assert.assertTrue(t.blockUntilEnded(TIMEOUT));
   Assert.assertEquals(messages, MutableList.of("1", "2", "main"));
   Assert.assertTrue(
       stopwatch.elapsed(TimeUnit.MILLISECONDS) < TIMEOUT.toMilliseconds(),
       "took too long: " + stopwatch);
   Assert.assertFalse(t.isError());
   Assert.assertTrue(t1.isError());
 }
  @Test
  public void testCancelled() throws InterruptedException, ExecutionException {
    Task<List<?>> t =
        Tasks.sequential(sayTask("1"), sayTask("2a", Duration.THIRTY_SECONDS, "2b"), sayTask("3"));
    ec.submit(t);
    synchronized (messages) {
      while (messages.size() <= 1) messages.wait();
    }
    Assert.assertEquals(messages, Arrays.asList("1", "2a"));
    Time.sleep(Duration.millis(50));
    t.cancel(true);
    Assert.assertTrue(t.isDone());
    // 2 should get cancelled, and invoke the cancellation semaphore
    // 3 should get cancelled and not run at all
    Assert.assertEquals(messages, Arrays.asList("1", "2a"));

    // Need to ensure that 2 has been started; race where we might cancel it before its run method
    // is even begun. Hence doing "2a; pause; 2b" where nothing is interruptable before pause.
    Assert.assertTrue(cancellations.tryAcquire(10, TimeUnit.SECONDS));

    Iterator<Task<?>> ci = ((HasTaskChildren) t).getChildren().iterator();
    Assert.assertEquals(ci.next().get(), "1");
    Task<?> task2 = ci.next();
    Assert.assertTrue(task2.isBegun());
    Assert.assertTrue(task2.isDone());
    Assert.assertTrue(task2.isCancelled());

    Task<?> task3 = ci.next();
    Assert.assertFalse(task3.isBegun());
    Assert.assertTrue(task2.isDone());
    Assert.assertTrue(task2.isCancelled());

    // but we do _not_ get a mutex from task3 as it does not run (is not interrupted)
    Assert.assertEquals(cancellations.availablePermits(), 0);
  }
 /**
  * attempts to resolve hostnameTarget from origin
  *
  * @return null if it definitively can't be resolved, best-effort IP address if possible, or blank
  *     if we could not run ssh or make sense of the output
  */
 public static String getResolvedAddress(
     Entity entity, SshMachineLocation origin, String hostnameTarget) {
   ProcessTaskWrapper<Integer> task =
       SshTasks.newSshExecTaskFactory(origin, "ping -c 1 -t 1 " + hostnameTarget)
           .summary("checking resolution of " + hostnameTarget)
           .allowingNonZeroExitCode()
           .newTask();
   DynamicTasks.queueIfPossible(task).orSubmitAndBlock(entity).asTask().blockUntilEnded();
   if (task.asTask().isError()) {
     log.warn(
         "ping could not be run, at "
             + entity
             + " / "
             + origin
             + ": "
             + Tasks.getError(task.asTask()));
     return "";
   }
   if (task.getExitCode() == null || task.getExitCode() != 0) {
     if (task.getExitCode() != null && task.getExitCode() < 10) {
       // small number means ping failed to resolve or ping the hostname
       log.debug(
           "not able to resolve "
               + hostnameTarget
               + " from "
               + origin
               + " for "
               + entity
               + " because exit code was "
               + task.getExitCode());
       return null;
     }
     // large number means ping probably did not run
     log.warn(
         "ping not run as expected, at "
             + entity
             + " / "
             + origin
             + " (code "
             + task.getExitCode()
             + "):\n"
             + task.getStdout().trim()
             + " --- "
             + task.getStderr().trim());
     return "";
   }
   String out = task.getStdout();
   try {
     String line1 = Strings.getFirstLine(out);
     String ip = Strings.getFragmentBetween(line1, "(", ")");
     if (Strings.isNonBlank(ip)) return ip;
   } catch (Exception e) {
     Exceptions.propagateIfFatal(e);
     /* ignore non-parseable output */
   }
   if (out.contains("127.0.0.1")) return "127.0.0.1";
   return "";
 }
Esempio n. 7
0
  @SuppressWarnings({"unchecked"})
  public void start() {
    // TODO Previous incarnation of this logged this logged polledSensors.keySet(), but we don't
    // know that anymore
    // Is that ok, are can we do better?

    if (log.isDebugEnabled())
      log.debug("Starting poll for {} (using {})", new Object[] {entity, this});
    if (running) {
      throw new IllegalStateException(
          String.format(
              "Attempt to start poller %s of entity %s when already running", this, entity));
    }

    running = true;

    for (final Callable<?> oneOffJob : oneOffJobs) {
      Task<?> task =
          Tasks.builder()
              .dynamic(false)
              .body((Callable<Object>) oneOffJob)
              .name("Poll")
              .description("One-time poll job " + oneOffJob)
              .build();
      oneOffTasks.add(((EntityInternal) entity).getExecutionContext().submit(task));
    }

    for (final PollJob<V> pollJob : pollJobs) {
      final String scheduleName = pollJob.handler.getDescription();
      if (pollJob.pollPeriod.compareTo(Duration.ZERO) > 0) {
        Callable<Task<?>> pollingTaskFactory =
            new Callable<Task<?>>() {
              public Task<?> call() {
                DynamicSequentialTask<Void> task =
                    new DynamicSequentialTask<Void>(
                        MutableMap.of("displayName", scheduleName, "entity", entity),
                        new Callable<Void>() {
                          public Void call() {
                            pollJob.wrappedJob.run();
                            return null;
                          }
                        });
                BrooklynTaskTags.setTransient(task);
                return task;
              }
            };
        ScheduledTask task =
            new ScheduledTask(MutableMap.of("period", pollJob.pollPeriod), pollingTaskFactory);
        tasks.add((ScheduledTask) Entities.submit(entity, task));
      } else {
        if (log.isDebugEnabled())
          log.debug(
              "Activating poll (but leaving off, as period {}) for {} (using {})",
              new Object[] {pollJob.pollPeriod, entity, this});
      }
    }
  }
  @Test
  public void testTaskBuilderUsingAddAllChildren() {
    Task<String> t =
        Tasks.<String>builder()
            .dynamic(true)
            .body(monitorableJob("main"))
            .addAll(ImmutableList.of(monitorableTask("1"), monitorableTask("2")))
            .build();
    ec.submit(t);
    releaseAndWaitForMonitorableJob("1");
    releaseAndWaitForMonitorableJob("2");
    releaseAndWaitForMonitorableJob("main");

    Assert.assertEquals(messages, MutableList.of("1", "2", "main"));
  }
  protected void beforeSubmit(Map<?, ?> flags, Task<?> task) {
    incompleteTaskCount.incrementAndGet();

    Task<?> currentTask = Tasks.current();
    if (currentTask != null) ((TaskInternal<?>) task).setSubmittedByTask(currentTask);
    ((TaskInternal<?>) task).setSubmitTimeUtc(System.currentTimeMillis());

    if (flags.get("tag") != null)
      ((TaskInternal<?>) task).getMutableTags().add(flags.remove("tag"));
    if (flags.get("tags") != null)
      ((TaskInternal<?>) task).getMutableTags().addAll((Collection<?>) flags.remove("tags"));

    for (Object tag : ((TaskInternal<?>) task).getTags()) {
      getMutableTasksWithTag(tag).add(task);
    }
  }
 /**
  * Waits for the given target to report service up, then runs the given task (often an invocation
  * on that entity), with the given name. If the target goes away, this task marks itself
  * inessential before failing so as not to cause a parent task to fail.
  */
 static <T> Task<T> whenServiceUp(final Entity target, final TaskAdaptable<T> task, String name) {
   return Tasks.<T>builder()
       .name(name)
       .dynamic(true)
       .body(
           new Callable<T>() {
             @Override
             public T call() {
               try {
                 while (true) {
                   if (!Entities.isManaged(target)) {
                     Tasks.markInessential();
                     throw new IllegalStateException("Target " + target + " is no longer managed");
                   }
                   if (Boolean.TRUE.equals(target.getAttribute(Attributes.SERVICE_UP))) {
                     Tasks.resetBlockingDetails();
                     TaskTags.markInessential(task);
                     DynamicTasks.queue(task);
                     try {
                       return task.asTask().getUnchecked();
                     } catch (Exception e) {
                       if (Entities.isManaged(target)) {
                         throw Exceptions.propagate(e);
                       } else {
                         Tasks.markInessential();
                         throw new IllegalStateException(
                             "Target " + target + " is no longer managed", e);
                       }
                     }
                   } else {
                     Tasks.setBlockingDetails("Waiting on " + target + " to be ready");
                   }
                   // TODO replace with subscription?
                   Time.sleep(Duration.ONE_SECOND);
                 }
               } finally {
                 Tasks.resetBlockingDetails();
               }
             }
           })
       .build();
 }
  @Override
  public void deploy(String url, String targetName) {
    checkNotNull(url, "url");
    checkNotNull(targetName, "targetName");
    targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName);

    // set it up so future nodes get the right wars
    addToWarsByContext(this, url, targetName);

    log.debug(
        "Deploying "
            + targetName
            + "->"
            + url
            + " across cluster "
            + this
            + "; WARs now "
            + getConfig(WARS_BY_CONTEXT));

    Iterable<CanDeployAndUndeploy> targets =
        Iterables.filter(getChildren(), CanDeployAndUndeploy.class);
    TaskBuilder<Void> tb =
        Tasks.<Void>builder()
            .parallel(true)
            .name("Deploy " + targetName + " to cluster (size " + Iterables.size(targets) + ")");
    for (Entity target : targets) {
      tb.add(
          whenServiceUp(
              target,
              Effectors.invocation(
                  target, DEPLOY, MutableMap.of("url", url, "targetName", targetName)),
              "Deploy " + targetName + " to " + target + " when ready"));
    }
    DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked();

    // Update attribute
    // TODO support for atomic sensor update (should be part of standard tooling; NB there is some
    // work towards this, according to @aledsage)
    Set<String> deployedWars = MutableSet.copyOf(getAttribute(DEPLOYED_WARS));
    deployedWars.add(targetName);
    setAttribute(DEPLOYED_WARS, deployedWars);
  }
  @Test
  public void testChildrenRunConcurrentlyWithPrimary() {
    Task<String> t =
        Tasks.<String>builder()
            .dynamic(true)
            .body(monitorableJob("main"))
            .add(monitorableTask("1"))
            .add(monitorableTask("2"))
            .build();
    ec.submit(t);
    releaseAndWaitForMonitorableJob("1");
    releaseAndWaitForMonitorableJob("main");
    Assert.assertFalse(t.blockUntilEnded(TINY_TIME));
    releaseMonitorableJob("2");

    Assert.assertTrue(t.blockUntilEnded(TIMEOUT));
    Assert.assertEquals(messages, MutableList.of("1", "main", "2"));
    Assert.assertTrue(
        stopwatch.elapsed(TimeUnit.MILLISECONDS) < TIMEOUT.toMilliseconds(),
        "took too long: " + stopwatch);
    Assert.assertFalse(t.isError());
  }
 public Task<String> sayTask(String message, Duration duration, String message2) {
   return Tasks.<String>builder().body(sayCallable(message, duration, message2)).build();
 }
Esempio n. 14
0
 /**
  * convenience for setting "blocking details" on any task where the current thread is running,
  * while the passed code is executed; often used from groovy as <code>
  *  withBlockingDetails("sleeping 5s") { Thread.sleep(5000); } </code>
  *
  * @deprecated in 0.4.0, use Tasks.withBlockingDetails
  */
 public static Object withBlockingDetails(String description, Callable code) throws Exception {
   return Tasks.withBlockingDetails(description, code);
 }
Esempio n. 15
0
 /**
  * convenience for setting "blocking details" on any task where the current thread is running;
  * typically invoked prior to a wait, for transparency to a user; then invoked with 'null' just
  * after the wait
  *
  * @deprecated in 0.4.0, use Tasks.setBlockingDetails
  */
 public static void setBlockingDetails(String description) {
   Tasks.setBlockingDetails(description);
 }
Esempio n. 16
0
 /** @deprecated in 0.4.0, use Tasks.current() */
 public static Task getCurrentTask() {
   return Tasks.current();
 }
 protected Task<String> monitorableTask(
     final Runnable pre, final String id, final Callable<String> post) {
   Task<String> t = Tasks.<String>builder().body(monitorableJob(pre, id, post)).build();
   monitorableTasksMap.put(id, t);
   return t;
 }
Esempio n. 18
0
  @Override
  public void install() {
    // will fail later if can't sudo (if sudo is required)
    DynamicTasks.queueIfPossible(SshTasks.dontRequireTtyForSudo(getMachine(), false))
        .orSubmitAndBlock();

    DownloadResolver nginxResolver = mgmt().getEntityDownloadsManager().newDownloader(this);
    List<String> nginxUrls = nginxResolver.getTargets();
    String nginxSaveAs = nginxResolver.getFilename();
    setExpandedInstallDir(
        getInstallDir()
            + "/"
            + nginxResolver.getUnpackedDirectoryName(format("nginx-%s", getVersion())));

    boolean sticky = ((NginxController) entity).isSticky();
    boolean isMac = getMachine().getOsDetails().isMac();

    MutableMap<String, String> installGccPackageFlags =
        MutableMap.of(
            "onlyifmissing", "gcc",
            "yum", "gcc",
            "apt", "gcc",
            "zypper", "gcc",
            "port", null);
    MutableMap<String, String> installMakePackageFlags =
        MutableMap.of(
            "onlyifmissing", "make",
            "yum", "make",
            "apt", "make",
            "zypper", "make",
            "port", null);
    MutableMap<String, String> installPackageFlags =
        MutableMap.of(
            "yum", "openssl-devel pcre-devel",
            "apt", "libssl-dev zlib1g-dev libpcre3-dev",
            "zypper", "libopenssl-devel pcre-devel",
            "port", null);

    String stickyModuleVersion = entity.getConfig(NginxController.STICKY_VERSION);
    DownloadResolver stickyModuleResolver =
        mgmt()
            .getEntityDownloadsManager()
            .newDownloader(
                this, "stickymodule", ImmutableMap.of("addonversion", stickyModuleVersion));
    List<String> stickyModuleUrls = stickyModuleResolver.getTargets();
    String stickyModuleSaveAs = stickyModuleResolver.getFilename();
    String stickyModuleExpandedInstallDir =
        String.format(
            "%s/src/%s",
            getExpandedInstallDir(),
            stickyModuleResolver.getUnpackedDirectoryName(
                "nginx-sticky-module-" + stickyModuleVersion));

    List<String> cmds = Lists.newArrayList();

    cmds.add(BashCommands.INSTALL_TAR);
    cmds.add(
        BashCommands.alternatives(
            BashCommands.ifExecutableElse0(
                "apt-get", BashCommands.installPackage("build-essential")),
            BashCommands.ifExecutableElse0(
                "yum",
                BashCommands.sudo("yum -y --nogpgcheck groupinstall \"Development Tools\""))));
    cmds.add(BashCommands.installPackage(installGccPackageFlags, "nginx-prerequisites-gcc"));
    cmds.add(BashCommands.installPackage(installMakePackageFlags, "nginx-prerequisites-make"));
    cmds.add(BashCommands.installPackage(installPackageFlags, "nginx-prerequisites"));
    cmds.addAll(BashCommands.commandsToDownloadUrlsAs(nginxUrls, nginxSaveAs));

    String pcreExpandedInstallDirname = "";
    if (isMac) {
      String pcreVersion = entity.getConfig(NginxController.PCRE_VERSION);
      DownloadResolver pcreResolver =
          mgmt()
              .getEntityDownloadsManager()
              .newDownloader(this, "pcre", ImmutableMap.of("addonversion", pcreVersion));
      List<String> pcreUrls = pcreResolver.getTargets();
      String pcreSaveAs = pcreResolver.getFilename();
      pcreExpandedInstallDirname = pcreResolver.getUnpackedDirectoryName("pcre-" + pcreVersion);

      // Install PCRE
      cmds.addAll(BashCommands.commandsToDownloadUrlsAs(pcreUrls, pcreSaveAs));
      cmds.add(format("mkdir -p %s/pcre-dist", getInstallDir()));
      cmds.add(format("tar xvzf %s", pcreSaveAs));
      cmds.add(format("cd %s", pcreExpandedInstallDirname));
      cmds.add(format("./configure --prefix=%s/pcre-dist", getInstallDir()));
      cmds.add("make");
      cmds.add("make install");
      cmds.add("cd ..");
    }

    cmds.add(format("tar xvzf %s", nginxSaveAs));
    cmds.add(format("cd %s", getExpandedInstallDir()));

    if (sticky) {
      cmds.add("cd src");
      cmds.addAll(BashCommands.commandsToDownloadUrlsAs(stickyModuleUrls, stickyModuleSaveAs));
      cmds.add(format("tar xvzf %s", stickyModuleSaveAs));
      cmds.add("cd ..");
    }

    // Note that for OS X, not including space after "-L" because broken in 10.6.8 (but fixed in
    // 10.7.x)
    //      see http://trac.nginx.org/nginx/ticket/227
    String withLdOpt = entity.getConfig(NginxController.WITH_LD_OPT);
    if (isMac)
      withLdOpt =
          format("-L%s/pcre-dist/lib", getInstallDir())
              + (Strings.isBlank(withLdOpt) ? "" : " " + withLdOpt);
    String withCcOpt = entity.getConfig(NginxController.WITH_CC_OPT);

    StringBuilder configureCommand =
        new StringBuilder("./configure")
            .append(format(" --prefix=%s/dist", getExpandedInstallDir()))
            .append(" --with-http_ssl_module")
            .append(sticky ? format(" --add-module=%s ", stickyModuleExpandedInstallDir) : "")
            .append(!Strings.isBlank(withLdOpt) ? format(" --with-ld-opt=\"%s\"", withLdOpt) : "")
            .append(!Strings.isBlank(withCcOpt) ? format(" --with-cc-opt=\"%s\"", withCcOpt) : "");
    if (isMac) {
      configureCommand
          .append(" --with-pcre=")
          .append(getInstallDir())
          .append("/")
          .append(pcreExpandedInstallDirname);
    }

    cmds.addAll(ImmutableList.of("mkdir -p dist", configureCommand.toString(), "make install"));

    ScriptHelper script =
        newScript(INSTALLING)
            .body
            .append(cmds)
            .header
            .prepend("set -x")
            .gatherOutput()
            .failOnNonZeroResultCode(false);

    int result = script.execute();

    if (result != 0) {
      String notes =
          "likely an error building nginx. consult the brooklyn log ssh output for further details.\n"
              + "note that this Brooklyn nginx driver compiles nginx from source. "
              + "it attempts to install common prerequisites but this does not always succeed.\n";
      OsDetails os = getMachine().getOsDetails();
      if (os.isMac()) {
        notes +=
            "deploying to Mac OS X, you will require Xcode and Xcode command-line tools, and on "
                + "some versions the pcre library (e.g. using macports, sudo port install pcre).\n";
      }
      if (os.isWindows()) {
        notes +=
            "this nginx driver is not designed for windows, unless cygwin is installed, and you are patient.\n";
      }
      if (getEntity().getApplication().getClass().getCanonicalName().startsWith("brooklyn.demo.")) {
        // this is maybe naughty ... but since we use nginx in the first demo example,
        // and since it's actually pretty complicated, let's give a little extra hand-holding
        notes +=
            "if debugging this is all a bit much and you just want to run a demo, "
                + "you have two fairly friendly options.\n"
                + "1. you can use a well known cloud, like AWS or Rackspace, where this should run "
                + "in a tried-and-tested Ubuntu or CentOS environment, without any problems "
                + "(and if it does let us know and we'll fix it!).\n"
                + "2. or you can just use the demo without nginx, instead access the appserver instances directly.\n";
      }

      if (!script.getResultStderr().isEmpty()) {
        notes += "\n" + "STDERR\n" + script.getResultStderr() + "\n";
        Streams.logStreamTail(
            log,
            "STDERR of problem in " + Tasks.current(),
            Streams.byteArrayOfString(script.getResultStderr()),
            1024);
      }
      if (!script.getResultStdout().isEmpty()) {
        notes += "\n" + "STDOUT\n" + script.getResultStdout() + "\n";
        Streams.logStreamTail(
            log,
            "STDOUT of problem in " + Tasks.current(),
            Streams.byteArrayOfString(script.getResultStdout()),
            1024);
      }

      Tasks.setExtraStatusDetails(notes.trim());

      throw new IllegalStateException(
          "Installation of nginx failed (shell returned non-zero result " + result + ")");
    }
  }