Repository: brooklyn-library Updated Branches: refs/heads/master 9fdf1dbf6 -> b8395ff30
Moving Ansible as a module so it matches the new CM structure Project: http://git-wip-us.apache.org/repos/asf/brooklyn-library/repo Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-library/commit/c1a87838 Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-library/tree/c1a87838 Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-library/diff/c1a87838 Branch: refs/heads/master Commit: c1a87838197f172ec5179831ba3824f0ead117a4 Parents: 121db32 Author: Yavor Yanchev <[email protected]> Authored: Tue Feb 16 11:10:07 2016 +0200 Committer: Yavor Yanchev <[email protected]> Committed: Wed Feb 17 11:43:03 2016 +0200 ---------------------------------------------------------------------- software/cm/ansible/pom.xml | 84 ++++++++ .../entity/cm/ansible/AnsibleBashCommands.java | 38 ++++ .../entity/cm/ansible/AnsibleConfig.java | 66 ++++++ .../entity/cm/ansible/AnsibleEntity.java | 40 ++++ .../entity/cm/ansible/AnsibleEntityImpl.java | 61 ++++++ .../ansible/AnsibleLifecycleEffectorTasks.java | 203 +++++++++++++++++++ .../entity/cm/ansible/AnsiblePlaybookTasks.java | 109 ++++++++++ .../ansible/AnsibleEntityIntegrationTest.java | 116 +++++++++++ software/cm/pom.xml | 53 +---- .../entity/cm/ansible/AnsibleBashCommands.java | 38 ---- .../entity/cm/ansible/AnsibleConfig.java | 66 ------ .../entity/cm/ansible/AnsibleEntity.java | 40 ---- .../entity/cm/ansible/AnsibleEntityImpl.java | 61 ------ .../ansible/AnsibleLifecycleEffectorTasks.java | 203 ------------------- .../entity/cm/ansible/AnsiblePlaybookTasks.java | 109 ---------- .../ansible/AnsibleEntityIntegrationTest.java | 116 ----------- 16 files changed, 718 insertions(+), 685 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/pom.xml ---------------------------------------------------------------------- diff --git a/software/cm/ansible/pom.xml b/software/cm/ansible/pom.xml new file mode 100644 index 0000000..028d73d --- /dev/null +++ b/software/cm/ansible/pom.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <artifactId>brooklyn-software-cm-ansible</artifactId> + <packaging>jar</packaging> + <name>Brooklyn CM Ansible</name> + <description>Brooklyn entities for Configuration Management using Ansible</description> + + + <parent> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-library</artifactId> + <version>0.9.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION --> + <relativePath>../../../pom.xml</relativePath> + </parent> + + <dependencies> + <dependency> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-software-base</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-camp</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-test-support</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-core</artifactId> + <version>${project.version}</version> + <classifier>tests</classifier> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.brooklyn</groupId> + <artifactId>brooklyn-software-base</artifactId> + <version>${project.version}</version> + <classifier>tests</classifier> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>${assertj.version}</version> + <scope>test</scope> + </dependency> + + </dependencies> + +</project> http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java new file mode 100644 index 0000000..e22ee34 --- /dev/null +++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_CURL; +import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_TAR; +import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_UNZIP; +import static org.apache.brooklyn.util.ssh.BashCommands.installExecutable; +import static org.apache.brooklyn.util.ssh.BashCommands.sudo; + +import org.apache.brooklyn.util.ssh.BashCommands; + +public class AnsibleBashCommands { + + public static final String INSTALL_ANSIBLE = + BashCommands.chain( + sudo("apt-add-repository -y ppa:ansible/ansible"), + INSTALL_CURL, + INSTALL_TAR, + INSTALL_UNZIP, + installExecutable("ansible")); +} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java new file mode 100644 index 0000000..d63c2c6 --- /dev/null +++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.util.core.flags.SetFromFlag; + +import com.google.common.annotations.Beta; + +/** {@link ConfigKey}s used to configure Ansible */ +@Beta +public interface AnsibleConfig { + + enum AnsibleModes { + PLAYBOOK + }; + + @SetFromFlag("playbook") + ConfigKey<String> ANSIBLE_PLAYBOOK = ConfigKeys.newStringConfigKey("brooklyn.ansible.playbook", + "Playbook to be execute by Ansible"); + + @SetFromFlag("playbook.yaml") + ConfigKey<String> ANSIBLE_PLAYBOOK_YAML = ConfigKeys.newStringConfigKey("brooklyn.ansible.playbookYaml", + "Playbook to be execute by Ansible"); + + @SetFromFlag("playbook.url") + ConfigKey<String> ANSIBLE_PLAYBOOK_URL = ConfigKeys.newStringConfigKey("brooklyn.ansible.playbookUrl"); + + @SetFromFlag("ansible.service.start") + ConfigKey<String> ANSIBLE_SERVICE_START = ConfigKeys.newStringConfigKey("ansible.service.start", + "Default start command used with conjunction with the Ansible's service module", + "sudo ansible localhost -c local -m service -a \"name=%s state=started\""); + + @SetFromFlag("ansible.service.stop") + ConfigKey<String> ANSIBLE_SERVICE_STOP = ConfigKeys.newStringConfigKey("ansible.service.stop", + "Default stop command used with conjunction with the Ansible's service module", + "sudo ansible localhost -c local -m service -a \"name=%s state=stopped\""); + + @SetFromFlag("ansible.service.checkPort") + ConfigKey<Integer> ANSIBLE_SERVICE_CHECK_PORT = ConfigKeys.newIntegerConfigKey("ansible.service.check.port"); + + @SetFromFlag("service.name") + ConfigKey<String> SERVICE_NAME = ConfigKeys.newStringConfigKey("brooklyn.ansible.serviceName", + "Name of OS service this will run as, for use in checking running and stopping"); + + @SetFromFlag("ansible.vars") + ConfigKey<Object> ANSIBLE_VARS = ConfigKeys.newConfigKey(Object.class, "brooklyn.ansible.vars", + "Ansible 'extra-vars' variable configuration values"); +} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java new file mode 100644 index 0000000..a2c9676 --- /dev/null +++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import org.apache.brooklyn.api.entity.ImplementedBy; +import org.apache.brooklyn.core.annotation.Effector; +import org.apache.brooklyn.core.annotation.EffectorParam; +import org.apache.brooklyn.core.effector.MethodEffector; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; + +@ImplementedBy(AnsibleEntityImpl.class) +public interface AnsibleEntity extends SoftwareProcess, AnsibleConfig { + + MethodEffector<String> ANSIBLE_COMMAND = new MethodEffector<>(AnsibleEntity.class, "ansibleCommand"); + + @Effector(description = "Invoke an arbitrary Ansible command, optionally specifying the module (default is 'command')") + String ansibleCommand( + @EffectorParam(name="module", description = "Name of the Ansible module to invoke", defaultValue = "command") + String module, + @EffectorParam(name="args", description = "Arguments for the ansible command") + String args + ); + +} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java new file mode 100644 index 0000000..c2635ca --- /dev/null +++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import org.apache.brooklyn.entity.stock.EffectorStartableImpl; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; +import org.apache.brooklyn.util.text.Strings; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class AnsibleEntityImpl extends EffectorStartableImpl implements AnsibleEntity { + + private AnsibleLifecycleEffectorTasks lifecycleTasks; + + public void init() { + checkNotNull(getConfig(SERVICE_NAME), "service name is missing. it has to be provided by the user"); + String playbookName = getConfig(ANSIBLE_PLAYBOOK); + if (!Strings.isBlank(playbookName)) setDefaultDisplayName(playbookName + " (ansible)"); + + super.init(); + + lifecycleTasks = new AnsibleLifecycleEffectorTasks(); + + lifecycleTasks.attachLifecycleEffectors(this); + } + + @Override + public void populateServiceNotUpDiagnostics() { + // TODO no-op currently; should check ssh'able etc + } + + @Override + public String ansibleCommand(String module, String args) { + final ProcessTaskWrapper<Integer> command = DynamicTasks.queue( + AnsiblePlaybookTasks.moduleCommand(module, config().get(ANSIBLE_VARS), lifecycleTasks.getRunDir(), args)); + command.asTask().blockUntilEnded(); + if (0 == command.getExitCode()) { + return command.getStdout(); + } else { + throw new RuntimeException("Command (" + args + ") in module " + module + + " failed with stderr:\n" + command.getStderr() + "\n"); + } + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java new file mode 100644 index 0000000..e1a8622 --- /dev/null +++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import com.google.common.base.Supplier; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.location.MachineLocation; +import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; +import org.apache.brooklyn.core.entity.Attributes; +import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; +import org.apache.brooklyn.core.entity.trait.Startable; +import org.apache.brooklyn.core.location.Locations; +import org.apache.brooklyn.core.location.Machines; +import org.apache.brooklyn.entity.software.base.SoftwareProcess; +import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks; +import org.apache.brooklyn.feed.ssh.SshFeed; +import org.apache.brooklyn.feed.ssh.SshPollConfig; +import org.apache.brooklyn.location.ssh.SshMachineLocation; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.net.Urls; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.time.Time; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AnsibleLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements AnsibleConfig { + + private static final Logger LOG = LoggerFactory.getLogger(AnsibleLifecycleEffectorTasks.class); + + protected String serviceName; + protected SshFeed serviceSshFeed; + + protected Object extraVars; + protected String baseDir; + protected String runDir; + + public AnsibleLifecycleEffectorTasks() { + } + + public String getServiceName() { + if (serviceName!=null) return serviceName; + return serviceName = entity().config().get(AnsibleConfig.SERVICE_NAME); + } + + public Object getExtraVars() { + if (extraVars != null) return extraVars; + return extraVars = entity().config().get(ANSIBLE_VARS); + } + + public String getBaseDir() { + if (null != baseDir) return baseDir; + return baseDir = MachineLifecycleEffectorTasks.resolveOnBoxDir(entity(), + Machines.findUniqueMachineLocation(entity().getLocations(), SshMachineLocation.class).get()); + } + + public String getRunDir() { + if (null != runDir) return runDir; + return runDir = Urls.mergePaths(getBaseDir(), "apps/"+entity().getApplicationId()+"/ansible/playbooks/" + +entity().getEntityType().getSimpleName()+"_"+entity().getId()); + } + + @Override + public void attachLifecycleEffectors(Entity entity) { + if (getServiceName()==null && getClass().equals(AnsibleLifecycleEffectorTasks.class)) { + // warn on incorrect usage + LOG.warn("Uses of "+getClass()+" must define a PID file or a service name (or subclass and override {start,stop} methods as per javadoc) " + + "in order for check-running and stop to work"); + } + super.attachLifecycleEffectors(entity); + } + + @Override + protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) { + startWithAnsibleAsync(); + + return "ansible start tasks submitted"; + } + + protected String getPlaybookName() { + return entity().config().get(ANSIBLE_PLAYBOOK); + } + + protected void startWithAnsibleAsync() { + String installDir = Urls.mergePaths(getBaseDir(), "installs/ansible"); + + String playbookUrl = entity().config().get(ANSIBLE_PLAYBOOK_URL); + String playbookYaml = entity().config().get(ANSIBLE_PLAYBOOK_YAML); + + if (Strings.isNonBlank(playbookUrl) && Strings.isNonBlank(playbookYaml)) { + throw new IllegalArgumentException("You can specify " + AnsibleConfig.ANSIBLE_PLAYBOOK_URL.getName() + + " or " + AnsibleConfig.ANSIBLE_PLAYBOOK_YAML.getName() + " but not both of them!"); + } + + DynamicTasks.queue(AnsiblePlaybookTasks.installAnsible(installDir, false)); + + if (getExtraVars() != null) { + DynamicTasks.queue(AnsiblePlaybookTasks.configureExtraVars(getRunDir(), extraVars, false)); + } + + if (Strings.isNonBlank(playbookUrl)) { + DynamicTasks.queue(AnsiblePlaybookTasks.installPlaybook(getRunDir(), getPlaybookName(), playbookUrl)); + } + + if (Strings.isNonBlank(playbookYaml)) { + DynamicTasks.queue(AnsiblePlaybookTasks.buildPlaybookFile(getRunDir(), getPlaybookName())); + } + DynamicTasks.queue(AnsiblePlaybookTasks.runAnsible(getRunDir(), getExtraVars(), getPlaybookName())); + } + + + protected void postStartCustom() { + boolean result = false; + result |= tryCheckStartService(); + + if (!result) { + LOG.warn("No way to check whether "+entity()+" is running; assuming yes"); + } + entity().sensors().set(SoftwareProcess.SERVICE_UP, true); + + Maybe<SshMachineLocation> machine = Locations.findUniqueSshMachineLocation(entity().getLocations()); + + if (machine.isPresent()) { + + String serviceName = String.format("[%s]%s", entity().config().get(SERVICE_NAME).substring(0, 1), + entity().config().get(SERVICE_NAME).substring(1)); + String checkCmd = String.format("ps -ef | grep %s", serviceName); + + Integer serviceCheckPort = entity().config().get(ANSIBLE_SERVICE_CHECK_PORT); + + if (serviceCheckPort != null) { + checkCmd = String.format("sudo ansible localhost -c local -m wait_for -a \"host=0.0.0.0 port=%d\"", serviceCheckPort); + } + serviceSshFeed = SshFeed.builder() + .entity(entity()) + .period(Duration.ONE_MINUTE) + .machine(machine.get()) + .poll(new SshPollConfig<Boolean>(Startable.SERVICE_UP) + .command(checkCmd) + .setOnSuccess(true) + .setOnFailureOrException(false)) + .build(); + + entity().feeds().addFeed(serviceSshFeed); + } else { + LOG.warn("Location(s) {} not an ssh-machine location, so not polling for status; setting serviceUp immediately", entity().getLocations()); + } + } + + protected boolean tryCheckStartService() { + if (getServiceName()==null) return false; + + // if it's still up after 5s assume we are good (default behaviour) + Time.sleep(Duration.FIVE_SECONDS); + if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh(String.format(entity().config().get(AnsibleConfig.ANSIBLE_SERVICE_START), getServiceName()))).get())) { + throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+getServiceName()+")"); + } + + return true; + } + + @Override + protected String stopProcessesAtMachine() { + boolean result = false; + result |= tryStopService(); + if (!result) { + throw new IllegalStateException("The process for "+entity()+" could not be stopped (no impl!)"); + } + return "stopped"; + } + + @Override + protected StopMachineDetails<Integer> stopAnyProvisionedMachines() { + return super.stopAnyProvisionedMachines(); + } + + protected boolean tryStopService() { + if (getServiceName()==null) return false; + int result = DynamicTasks.queue(SshEffectorTasks.ssh(String.format(entity().config().get(AnsibleConfig.ANSIBLE_SERVICE_STOP), getServiceName()))).get(); + if (0 == result) return true; + if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL) != Lifecycle.RUNNING) + return true; + + throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)"); + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java new file mode 100644 index 0000000..36076a1 --- /dev/null +++ b/software/cm/ansible/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.mgmt.TaskFactory; +import org.apache.brooklyn.core.effector.EffectorTasks; +import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory; +import org.apache.brooklyn.util.net.Urls; +import org.apache.brooklyn.util.ssh.BashCommands; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import static org.apache.brooklyn.util.ssh.BashCommands.sudo; + +public class AnsiblePlaybookTasks { + private static final Logger LOG = LoggerFactory.getLogger(AnsiblePlaybookTasks.class); + private static final String EXTRA_VARS_FILENAME = "extra_vars.yaml"; + + public static TaskFactory<?> installAnsible(String ansibleDirectory, boolean force) { + String installCmd = cdAndRun(ansibleDirectory, AnsibleBashCommands.INSTALL_ANSIBLE); + if (!force) installCmd = BashCommands.alternatives("which ansible", installCmd); + return SshEffectorTasks.ssh(installCmd).summary("install ansible"); + } + + public static TaskFactory<?> installPlaybook(final String ansibleDirectory, final String playbookName, final String playbookUrl) { + return Tasks.sequential("build ansible playbook file for "+playbookName, + SshEffectorTasks.put(ansibleDirectory + "/" + playbookName + ".yaml") + .contents(ResourceUtils.create().getResourceFromUrl(playbookUrl)) + .createDirectory()); + } + + protected static String cdAndRun(String targetDirectory, String command) { + return BashCommands.chain("mkdir -p "+targetDirectory, + "cd "+targetDirectory, + command); + } + + public static TaskFactory<?> buildPlaybookFile(final String ansibleDirectory, String playbook) { + Entity entity = EffectorTasks.findEntity(); + String yaml = entity.config().get(AnsibleConfig.ANSIBLE_PLAYBOOK_YAML); + + return Tasks.sequential("build ansible playbook file for "+ playbook, + SshEffectorTasks.put(Urls.mergePaths(ansibleDirectory) + "/" + playbook + ".yaml") + .contents(yaml).createDirectory()); + } + + public static TaskFactory<?> runAnsible(final String dir, Object extraVars, String playbookName) { + String cmd = String.format("sudo ansible-playbook -i \"localhost,\" -c local " + + optionalExtraVarsParameter(extraVars) + + " -s %s.yaml", playbookName); + + if (LOG.isDebugEnabled()) { + LOG.debug("Ansible command: {}", cmd); + } + + return SshEffectorTasks.ssh(cdAndRun(dir, cmd)). + summary("run ansible playbook for " + playbookName).requiringExitCodeZero(); + } + + public static ProcessTaskFactory<Integer> moduleCommand(String module, Object extraVars, String root, String args) { + final String command = "ansible localhost " + + optionalExtraVarsParameter(extraVars) + + " -m '" + module + "' -a '" + args + "'"; + return SshEffectorTasks.ssh(sudo(BashCommands.chain("cd " + root, command))) + .summary("ad-hoc: " + command).requiringExitCodeZero(); + } + + public static TaskFactory<?> configureExtraVars(String dir, Object extraVars, boolean force) { + DumperOptions options = new DumperOptions(); + options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED); + Yaml asYaml = new Yaml(options); + final String varsYaml = asYaml.dump(extraVars); + return SshEffectorTasks.put(Urls.mergePaths(dir, EXTRA_VARS_FILENAME)) + .contents(varsYaml) + .summary("install extra vars") + .createDirectory(); + } + + private static String optionalExtraVarsParameter(Object extraVars) { + if (null == extraVars || Strings.isBlank(extraVars.toString())) { + return ""; + } + return " --extra-vars \"@" + EXTRA_VARS_FILENAME + "\" "; + } +} + http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/ansible/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java ---------------------------------------------------------------------- diff --git a/software/cm/ansible/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java b/software/cm/ansible/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java new file mode 100644 index 0000000..76e993d --- /dev/null +++ b/software/cm/ansible/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.entity.cm.ansible; + +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.MachineDetails; +import org.apache.brooklyn.api.location.OsDetails; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.EntityAsserts; +import org.apache.brooklyn.core.entity.trait.Startable; +import org.apache.brooklyn.core.location.BasicMachineDetails; +import org.apache.brooklyn.core.test.entity.TestApplication; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation; +import org.apache.brooklyn.location.ssh.SshMachineLocation; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +public class AnsibleEntityIntegrationTest { + protected TestApplication app; + protected LocalhostMachineProvisioningLocation testLocation; + protected AnsibleEntity ansible; + protected SshMachineLocation sshHost; + + String playbookYaml = Joiner.on("\n").join( + "---", + "- hosts: localhost", + " sudo: True", + "", + " tasks:", + " - apt: name=apache2 state=latest", + " when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'", + "", + " - yum: name=httpd state=latest", + " when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'", + "", + " - service: name=apache2 state=started enabled=no", + " when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'", + "", + " - service: name=httpd state=started enabled=no", + " when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'"); + + @BeforeMethod(alwaysRun = true) + public void setup() throws Exception { + app = TestApplication.Factory.newManagedInstanceForTests();; + testLocation = new LocalhostMachineProvisioningLocation(); + sshHost = testLocation.obtain(); + } + + @AfterMethod(alwaysRun = true) + public void shutdown() { + Entities.destroyAll(app.getManagementContext()); + } + + @Test(groups = {"Integration"}) + public void testAnsible() { + Task<BasicMachineDetails> detailsTask = app.getExecutionContext().submit( + BasicMachineDetails.taskForSshMachineLocation(sshHost)); + MachineDetails machine = detailsTask.getUnchecked(); + + + OsDetails details = machine.getOsDetails(); + + String osName = details.getName(); + String playbookServiceName = getPlaybookServiceName(osName); + ansible = app.createAndManageChild(EntitySpec.create(AnsibleEntity.class) + .configure("playbook.yaml", playbookYaml) + .configure("playbook", playbookServiceName) + .configure("service.name", playbookServiceName)); + + app.start(ImmutableList.of(testLocation)); + EntityAsserts.assertAttributeEqualsEventually(ansible, Startable.SERVICE_UP, true); + + ansible.stop(); + EntityAsserts.assertAttributeEqualsEventually(ansible, Startable.SERVICE_UP, false); + } + + private String getPlaybookServiceName(String os) { + String name; + + switch (os.toLowerCase()) { + case "fedora": + name = "httpd"; + break; + case "centos": + name = "httpd"; + break; + default: + name = "apache2"; + } + return name; + } +} + + http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/pom.xml ---------------------------------------------------------------------- diff --git a/software/cm/pom.xml b/software/cm/pom.xml index ea17774..8a86fb7 100644 --- a/software/cm/pom.xml +++ b/software/cm/pom.xml @@ -70,61 +70,10 @@ </mailingList> </mailingLists> - - <dependencies> - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-api</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-software-base</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-software-database</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-camp</artifactId> - <version>${project.version}</version> - </dependency> - - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-test-support</artifactId> - <version>${project.version}</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-core</artifactId> - <version>${project.version}</version> - <classifier>tests</classifier> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.brooklyn</groupId> - <artifactId>brooklyn-software-base</artifactId> - <version>${project.version}</version> - <classifier>tests</classifier> - <scope>test</scope> - </dependency> - - <dependency> - <groupId>org.assertj</groupId> - <artifactId>assertj-core</artifactId> - <version>${assertj.version}</version> - <scope>test</scope> - </dependency> - </dependencies> - <modules> <module>salt</module> + <module>ansible</module> </modules> http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java ---------------------------------------------------------------------- diff --git a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java b/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java deleted file mode 100644 index e22ee34..0000000 --- a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleBashCommands.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_CURL; -import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_TAR; -import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_UNZIP; -import static org.apache.brooklyn.util.ssh.BashCommands.installExecutable; -import static org.apache.brooklyn.util.ssh.BashCommands.sudo; - -import org.apache.brooklyn.util.ssh.BashCommands; - -public class AnsibleBashCommands { - - public static final String INSTALL_ANSIBLE = - BashCommands.chain( - sudo("apt-add-repository -y ppa:ansible/ansible"), - INSTALL_CURL, - INSTALL_TAR, - INSTALL_UNZIP, - installExecutable("ansible")); -} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java ---------------------------------------------------------------------- diff --git a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java b/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java deleted file mode 100644 index d63c2c6..0000000 --- a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import org.apache.brooklyn.config.ConfigKey; -import org.apache.brooklyn.core.config.ConfigKeys; -import org.apache.brooklyn.util.core.flags.SetFromFlag; - -import com.google.common.annotations.Beta; - -/** {@link ConfigKey}s used to configure Ansible */ -@Beta -public interface AnsibleConfig { - - enum AnsibleModes { - PLAYBOOK - }; - - @SetFromFlag("playbook") - ConfigKey<String> ANSIBLE_PLAYBOOK = ConfigKeys.newStringConfigKey("brooklyn.ansible.playbook", - "Playbook to be execute by Ansible"); - - @SetFromFlag("playbook.yaml") - ConfigKey<String> ANSIBLE_PLAYBOOK_YAML = ConfigKeys.newStringConfigKey("brooklyn.ansible.playbookYaml", - "Playbook to be execute by Ansible"); - - @SetFromFlag("playbook.url") - ConfigKey<String> ANSIBLE_PLAYBOOK_URL = ConfigKeys.newStringConfigKey("brooklyn.ansible.playbookUrl"); - - @SetFromFlag("ansible.service.start") - ConfigKey<String> ANSIBLE_SERVICE_START = ConfigKeys.newStringConfigKey("ansible.service.start", - "Default start command used with conjunction with the Ansible's service module", - "sudo ansible localhost -c local -m service -a \"name=%s state=started\""); - - @SetFromFlag("ansible.service.stop") - ConfigKey<String> ANSIBLE_SERVICE_STOP = ConfigKeys.newStringConfigKey("ansible.service.stop", - "Default stop command used with conjunction with the Ansible's service module", - "sudo ansible localhost -c local -m service -a \"name=%s state=stopped\""); - - @SetFromFlag("ansible.service.checkPort") - ConfigKey<Integer> ANSIBLE_SERVICE_CHECK_PORT = ConfigKeys.newIntegerConfigKey("ansible.service.check.port"); - - @SetFromFlag("service.name") - ConfigKey<String> SERVICE_NAME = ConfigKeys.newStringConfigKey("brooklyn.ansible.serviceName", - "Name of OS service this will run as, for use in checking running and stopping"); - - @SetFromFlag("ansible.vars") - ConfigKey<Object> ANSIBLE_VARS = ConfigKeys.newConfigKey(Object.class, "brooklyn.ansible.vars", - "Ansible 'extra-vars' variable configuration values"); -} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java ---------------------------------------------------------------------- diff --git a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java b/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java deleted file mode 100644 index a2c9676..0000000 --- a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntity.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import org.apache.brooklyn.api.entity.ImplementedBy; -import org.apache.brooklyn.core.annotation.Effector; -import org.apache.brooklyn.core.annotation.EffectorParam; -import org.apache.brooklyn.core.effector.MethodEffector; -import org.apache.brooklyn.entity.software.base.SoftwareProcess; - -@ImplementedBy(AnsibleEntityImpl.class) -public interface AnsibleEntity extends SoftwareProcess, AnsibleConfig { - - MethodEffector<String> ANSIBLE_COMMAND = new MethodEffector<>(AnsibleEntity.class, "ansibleCommand"); - - @Effector(description = "Invoke an arbitrary Ansible command, optionally specifying the module (default is 'command')") - String ansibleCommand( - @EffectorParam(name="module", description = "Name of the Ansible module to invoke", defaultValue = "command") - String module, - @EffectorParam(name="args", description = "Arguments for the ansible command") - String args - ); - -} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java ---------------------------------------------------------------------- diff --git a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java b/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java deleted file mode 100644 index c2635ca..0000000 --- a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityImpl.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import org.apache.brooklyn.entity.stock.EffectorStartableImpl; -import org.apache.brooklyn.util.core.task.DynamicTasks; -import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; -import org.apache.brooklyn.util.text.Strings; - -import static com.google.common.base.Preconditions.checkNotNull; - -public class AnsibleEntityImpl extends EffectorStartableImpl implements AnsibleEntity { - - private AnsibleLifecycleEffectorTasks lifecycleTasks; - - public void init() { - checkNotNull(getConfig(SERVICE_NAME), "service name is missing. it has to be provided by the user"); - String playbookName = getConfig(ANSIBLE_PLAYBOOK); - if (!Strings.isBlank(playbookName)) setDefaultDisplayName(playbookName + " (ansible)"); - - super.init(); - - lifecycleTasks = new AnsibleLifecycleEffectorTasks(); - - lifecycleTasks.attachLifecycleEffectors(this); - } - - @Override - public void populateServiceNotUpDiagnostics() { - // TODO no-op currently; should check ssh'able etc - } - - @Override - public String ansibleCommand(String module, String args) { - final ProcessTaskWrapper<Integer> command = DynamicTasks.queue( - AnsiblePlaybookTasks.moduleCommand(module, config().get(ANSIBLE_VARS), lifecycleTasks.getRunDir(), args)); - command.asTask().blockUntilEnded(); - if (0 == command.getExitCode()) { - return command.getStdout(); - } else { - throw new RuntimeException("Command (" + args + ") in module " + module - + " failed with stderr:\n" + command.getStderr() + "\n"); - } - } -} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java ---------------------------------------------------------------------- diff --git a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java b/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java deleted file mode 100644 index e1a8622..0000000 --- a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsibleLifecycleEffectorTasks.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import com.google.common.base.Supplier; -import org.apache.brooklyn.api.entity.Entity; -import org.apache.brooklyn.api.location.MachineLocation; -import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; -import org.apache.brooklyn.core.entity.Attributes; -import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; -import org.apache.brooklyn.core.entity.trait.Startable; -import org.apache.brooklyn.core.location.Locations; -import org.apache.brooklyn.core.location.Machines; -import org.apache.brooklyn.entity.software.base.SoftwareProcess; -import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks; -import org.apache.brooklyn.feed.ssh.SshFeed; -import org.apache.brooklyn.feed.ssh.SshPollConfig; -import org.apache.brooklyn.location.ssh.SshMachineLocation; -import org.apache.brooklyn.util.core.task.DynamicTasks; -import org.apache.brooklyn.util.guava.Maybe; -import org.apache.brooklyn.util.net.Urls; -import org.apache.brooklyn.util.text.Strings; -import org.apache.brooklyn.util.time.Duration; -import org.apache.brooklyn.util.time.Time; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AnsibleLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements AnsibleConfig { - - private static final Logger LOG = LoggerFactory.getLogger(AnsibleLifecycleEffectorTasks.class); - - protected String serviceName; - protected SshFeed serviceSshFeed; - - protected Object extraVars; - protected String baseDir; - protected String runDir; - - public AnsibleLifecycleEffectorTasks() { - } - - public String getServiceName() { - if (serviceName!=null) return serviceName; - return serviceName = entity().config().get(AnsibleConfig.SERVICE_NAME); - } - - public Object getExtraVars() { - if (extraVars != null) return extraVars; - return extraVars = entity().config().get(ANSIBLE_VARS); - } - - public String getBaseDir() { - if (null != baseDir) return baseDir; - return baseDir = MachineLifecycleEffectorTasks.resolveOnBoxDir(entity(), - Machines.findUniqueMachineLocation(entity().getLocations(), SshMachineLocation.class).get()); - } - - public String getRunDir() { - if (null != runDir) return runDir; - return runDir = Urls.mergePaths(getBaseDir(), "apps/"+entity().getApplicationId()+"/ansible/playbooks/" - +entity().getEntityType().getSimpleName()+"_"+entity().getId()); - } - - @Override - public void attachLifecycleEffectors(Entity entity) { - if (getServiceName()==null && getClass().equals(AnsibleLifecycleEffectorTasks.class)) { - // warn on incorrect usage - LOG.warn("Uses of "+getClass()+" must define a PID file or a service name (or subclass and override {start,stop} methods as per javadoc) " + - "in order for check-running and stop to work"); - } - super.attachLifecycleEffectors(entity); - } - - @Override - protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) { - startWithAnsibleAsync(); - - return "ansible start tasks submitted"; - } - - protected String getPlaybookName() { - return entity().config().get(ANSIBLE_PLAYBOOK); - } - - protected void startWithAnsibleAsync() { - String installDir = Urls.mergePaths(getBaseDir(), "installs/ansible"); - - String playbookUrl = entity().config().get(ANSIBLE_PLAYBOOK_URL); - String playbookYaml = entity().config().get(ANSIBLE_PLAYBOOK_YAML); - - if (Strings.isNonBlank(playbookUrl) && Strings.isNonBlank(playbookYaml)) { - throw new IllegalArgumentException("You can specify " + AnsibleConfig.ANSIBLE_PLAYBOOK_URL.getName() - + " or " + AnsibleConfig.ANSIBLE_PLAYBOOK_YAML.getName() + " but not both of them!"); - } - - DynamicTasks.queue(AnsiblePlaybookTasks.installAnsible(installDir, false)); - - if (getExtraVars() != null) { - DynamicTasks.queue(AnsiblePlaybookTasks.configureExtraVars(getRunDir(), extraVars, false)); - } - - if (Strings.isNonBlank(playbookUrl)) { - DynamicTasks.queue(AnsiblePlaybookTasks.installPlaybook(getRunDir(), getPlaybookName(), playbookUrl)); - } - - if (Strings.isNonBlank(playbookYaml)) { - DynamicTasks.queue(AnsiblePlaybookTasks.buildPlaybookFile(getRunDir(), getPlaybookName())); - } - DynamicTasks.queue(AnsiblePlaybookTasks.runAnsible(getRunDir(), getExtraVars(), getPlaybookName())); - } - - - protected void postStartCustom() { - boolean result = false; - result |= tryCheckStartService(); - - if (!result) { - LOG.warn("No way to check whether "+entity()+" is running; assuming yes"); - } - entity().sensors().set(SoftwareProcess.SERVICE_UP, true); - - Maybe<SshMachineLocation> machine = Locations.findUniqueSshMachineLocation(entity().getLocations()); - - if (machine.isPresent()) { - - String serviceName = String.format("[%s]%s", entity().config().get(SERVICE_NAME).substring(0, 1), - entity().config().get(SERVICE_NAME).substring(1)); - String checkCmd = String.format("ps -ef | grep %s", serviceName); - - Integer serviceCheckPort = entity().config().get(ANSIBLE_SERVICE_CHECK_PORT); - - if (serviceCheckPort != null) { - checkCmd = String.format("sudo ansible localhost -c local -m wait_for -a \"host=0.0.0.0 port=%d\"", serviceCheckPort); - } - serviceSshFeed = SshFeed.builder() - .entity(entity()) - .period(Duration.ONE_MINUTE) - .machine(machine.get()) - .poll(new SshPollConfig<Boolean>(Startable.SERVICE_UP) - .command(checkCmd) - .setOnSuccess(true) - .setOnFailureOrException(false)) - .build(); - - entity().feeds().addFeed(serviceSshFeed); - } else { - LOG.warn("Location(s) {} not an ssh-machine location, so not polling for status; setting serviceUp immediately", entity().getLocations()); - } - } - - protected boolean tryCheckStartService() { - if (getServiceName()==null) return false; - - // if it's still up after 5s assume we are good (default behaviour) - Time.sleep(Duration.FIVE_SECONDS); - if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh(String.format(entity().config().get(AnsibleConfig.ANSIBLE_SERVICE_START), getServiceName()))).get())) { - throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+getServiceName()+")"); - } - - return true; - } - - @Override - protected String stopProcessesAtMachine() { - boolean result = false; - result |= tryStopService(); - if (!result) { - throw new IllegalStateException("The process for "+entity()+" could not be stopped (no impl!)"); - } - return "stopped"; - } - - @Override - protected StopMachineDetails<Integer> stopAnyProvisionedMachines() { - return super.stopAnyProvisionedMachines(); - } - - protected boolean tryStopService() { - if (getServiceName()==null) return false; - int result = DynamicTasks.queue(SshEffectorTasks.ssh(String.format(entity().config().get(AnsibleConfig.ANSIBLE_SERVICE_STOP), getServiceName()))).get(); - if (0 == result) return true; - if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL) != Lifecycle.RUNNING) - return true; - - throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)"); - } -} http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java ---------------------------------------------------------------------- diff --git a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java b/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java deleted file mode 100644 index 36076a1..0000000 --- a/software/cm/src/main/java/org/apache/brooklyn/entity/cm/ansible/AnsiblePlaybookTasks.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import org.apache.brooklyn.api.entity.Entity; -import org.apache.brooklyn.api.mgmt.TaskFactory; -import org.apache.brooklyn.core.effector.EffectorTasks; -import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; -import org.apache.brooklyn.util.core.ResourceUtils; -import org.apache.brooklyn.util.core.task.Tasks; -import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory; -import org.apache.brooklyn.util.net.Urls; -import org.apache.brooklyn.util.ssh.BashCommands; -import org.apache.brooklyn.util.text.Strings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.Yaml; - -import static org.apache.brooklyn.util.ssh.BashCommands.sudo; - -public class AnsiblePlaybookTasks { - private static final Logger LOG = LoggerFactory.getLogger(AnsiblePlaybookTasks.class); - private static final String EXTRA_VARS_FILENAME = "extra_vars.yaml"; - - public static TaskFactory<?> installAnsible(String ansibleDirectory, boolean force) { - String installCmd = cdAndRun(ansibleDirectory, AnsibleBashCommands.INSTALL_ANSIBLE); - if (!force) installCmd = BashCommands.alternatives("which ansible", installCmd); - return SshEffectorTasks.ssh(installCmd).summary("install ansible"); - } - - public static TaskFactory<?> installPlaybook(final String ansibleDirectory, final String playbookName, final String playbookUrl) { - return Tasks.sequential("build ansible playbook file for "+playbookName, - SshEffectorTasks.put(ansibleDirectory + "/" + playbookName + ".yaml") - .contents(ResourceUtils.create().getResourceFromUrl(playbookUrl)) - .createDirectory()); - } - - protected static String cdAndRun(String targetDirectory, String command) { - return BashCommands.chain("mkdir -p "+targetDirectory, - "cd "+targetDirectory, - command); - } - - public static TaskFactory<?> buildPlaybookFile(final String ansibleDirectory, String playbook) { - Entity entity = EffectorTasks.findEntity(); - String yaml = entity.config().get(AnsibleConfig.ANSIBLE_PLAYBOOK_YAML); - - return Tasks.sequential("build ansible playbook file for "+ playbook, - SshEffectorTasks.put(Urls.mergePaths(ansibleDirectory) + "/" + playbook + ".yaml") - .contents(yaml).createDirectory()); - } - - public static TaskFactory<?> runAnsible(final String dir, Object extraVars, String playbookName) { - String cmd = String.format("sudo ansible-playbook -i \"localhost,\" -c local " - + optionalExtraVarsParameter(extraVars) - + " -s %s.yaml", playbookName); - - if (LOG.isDebugEnabled()) { - LOG.debug("Ansible command: {}", cmd); - } - - return SshEffectorTasks.ssh(cdAndRun(dir, cmd)). - summary("run ansible playbook for " + playbookName).requiringExitCodeZero(); - } - - public static ProcessTaskFactory<Integer> moduleCommand(String module, Object extraVars, String root, String args) { - final String command = "ansible localhost " - + optionalExtraVarsParameter(extraVars) - + " -m '" + module + "' -a '" + args + "'"; - return SshEffectorTasks.ssh(sudo(BashCommands.chain("cd " + root, command))) - .summary("ad-hoc: " + command).requiringExitCodeZero(); - } - - public static TaskFactory<?> configureExtraVars(String dir, Object extraVars, boolean force) { - DumperOptions options = new DumperOptions(); - options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED); - Yaml asYaml = new Yaml(options); - final String varsYaml = asYaml.dump(extraVars); - return SshEffectorTasks.put(Urls.mergePaths(dir, EXTRA_VARS_FILENAME)) - .contents(varsYaml) - .summary("install extra vars") - .createDirectory(); - } - - private static String optionalExtraVarsParameter(Object extraVars) { - if (null == extraVars || Strings.isBlank(extraVars.toString())) { - return ""; - } - return " --extra-vars \"@" + EXTRA_VARS_FILENAME + "\" "; - } -} - http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/c1a87838/software/cm/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java ---------------------------------------------------------------------- diff --git a/software/cm/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java b/software/cm/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java deleted file mode 100644 index 76e993d..0000000 --- a/software/cm/src/test/java/org/apache/brooklyn/entity/cm/ansible/AnsibleEntityIntegrationTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.brooklyn.entity.cm.ansible; - -import org.apache.brooklyn.api.entity.EntitySpec; -import org.apache.brooklyn.api.location.MachineDetails; -import org.apache.brooklyn.api.location.OsDetails; -import org.apache.brooklyn.api.mgmt.Task; -import org.apache.brooklyn.core.entity.Entities; -import org.apache.brooklyn.core.entity.EntityAsserts; -import org.apache.brooklyn.core.entity.trait.Startable; -import org.apache.brooklyn.core.location.BasicMachineDetails; -import org.apache.brooklyn.core.test.entity.TestApplication; - -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; -import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation; -import org.apache.brooklyn.location.ssh.SshMachineLocation; - -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; - -public class AnsibleEntityIntegrationTest { - protected TestApplication app; - protected LocalhostMachineProvisioningLocation testLocation; - protected AnsibleEntity ansible; - protected SshMachineLocation sshHost; - - String playbookYaml = Joiner.on("\n").join( - "---", - "- hosts: localhost", - " sudo: True", - "", - " tasks:", - " - apt: name=apache2 state=latest", - " when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'", - "", - " - yum: name=httpd state=latest", - " when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'", - "", - " - service: name=apache2 state=started enabled=no", - " when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'", - "", - " - service: name=httpd state=started enabled=no", - " when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'"); - - @BeforeMethod(alwaysRun = true) - public void setup() throws Exception { - app = TestApplication.Factory.newManagedInstanceForTests();; - testLocation = new LocalhostMachineProvisioningLocation(); - sshHost = testLocation.obtain(); - } - - @AfterMethod(alwaysRun = true) - public void shutdown() { - Entities.destroyAll(app.getManagementContext()); - } - - @Test(groups = {"Integration"}) - public void testAnsible() { - Task<BasicMachineDetails> detailsTask = app.getExecutionContext().submit( - BasicMachineDetails.taskForSshMachineLocation(sshHost)); - MachineDetails machine = detailsTask.getUnchecked(); - - - OsDetails details = machine.getOsDetails(); - - String osName = details.getName(); - String playbookServiceName = getPlaybookServiceName(osName); - ansible = app.createAndManageChild(EntitySpec.create(AnsibleEntity.class) - .configure("playbook.yaml", playbookYaml) - .configure("playbook", playbookServiceName) - .configure("service.name", playbookServiceName)); - - app.start(ImmutableList.of(testLocation)); - EntityAsserts.assertAttributeEqualsEventually(ansible, Startable.SERVICE_UP, true); - - ansible.stop(); - EntityAsserts.assertAttributeEqualsEventually(ansible, Startable.SERVICE_UP, false); - } - - private String getPlaybookServiceName(String os) { - String name; - - switch (os.toLowerCase()) { - case "fedora": - name = "httpd"; - break; - case "centos": - name = "httpd"; - break; - default: - name = "apache2"; - } - return name; - } -} - -
