View Javadoc
1   package org.kuali.common.devops.ci;
2   
3   import static com.google.common.base.Preconditions.checkState;
4   import static com.google.common.base.Stopwatch.createStarted;
5   import static com.google.common.collect.Lists.newArrayList;
6   import static com.google.common.collect.Maps.newTreeMap;
7   import static java.lang.String.format;
8   import static java.util.Collections.reverse;
9   import static java.util.Collections.singletonList;
10  import static java.util.Collections.sort;
11  import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
12  import static org.kuali.common.devops.aws.NamedSecurityGroups.CI;
13  import static org.kuali.common.devops.aws.NamedSecurityGroups.CI_MASTER;
14  import static org.kuali.common.devops.ci.CreateBuildSlaveAMI.CI_SLAVE_STARTS_WITH_TOKEN;
15  import static org.kuali.common.devops.ci.CreateBuildSlaveAMI.getBasicLaunchRequest;
16  import static org.kuali.common.devops.ci.CreateBuildSlaveAMI.getCommonTags;
17  import static org.kuali.common.devops.ci.CreateBuildSlaveAMI.getEC2Service;
18  import static org.kuali.common.devops.ci.CreateBuildSlaveAMI.launchAndWait;
19  import static org.kuali.common.devops.ci.model.Constants.AES_PASSPHRASE_ENCRYPTED;
20  import static org.kuali.common.devops.ci.model.Constants.JDK6_VERSION;
21  import static org.kuali.common.devops.ci.model.Constants.JDK7_VERSION;
22  import static org.kuali.common.devops.ci.model.Constants.JDK8_VERSION;
23  import static org.kuali.common.devops.ci.model.Constants.JENKINS_VERSION;
24  import static org.kuali.common.devops.project.KualiDevOpsProjectConstants.KUALI_DEVOPS_PROJECT_IDENTIFIER;
25  import static org.kuali.common.dns.model.CNAMEContext.newCNAMEContext;
26  import static org.kuali.common.util.FormatUtils.getMillisAsInt;
27  import static org.kuali.common.util.FormatUtils.getTime;
28  import static org.kuali.common.util.base.Exceptions.illegalArgument;
29  import static org.kuali.common.util.encrypt.Encryption.getDefaultEncryptor;
30  import static org.kuali.common.util.log.LoggerLevel.INFO;
31  import static org.kuali.common.util.log.LoggerLevel.WARN;
32  import static org.kuali.common.util.maven.RepositoryUtils.getDefaultLocalRepository;
33  
34  import java.io.File;
35  import java.io.IOException;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.SortedMap;
39  
40  import org.codehaus.plexus.util.cli.StreamConsumer;
41  import org.junit.Test;
42  import org.kuali.common.aws.ec2.api.EC2Service;
43  import org.kuali.common.aws.ec2.model.AMI;
44  import org.kuali.common.aws.ec2.model.Distro;
45  import org.kuali.common.aws.ec2.model.RootVolume;
46  import org.kuali.common.aws.ec2.model.security.KualiSecurityGroup;
47  import org.kuali.common.core.ssh.KeyPair;
48  import org.kuali.common.core.system.VirtualSystem;
49  import org.kuali.common.devops.aws.Tags;
50  import org.kuali.common.devops.ci.CreateBuildSlaveAMI.ImageTagsComparator;
51  import org.kuali.common.devops.ci.model.BasicLaunchRequest;
52  import org.kuali.common.devops.ci.model.Constants;
53  import org.kuali.common.devops.ci.model.JenkinsContext;
54  import org.kuali.common.devops.logic.Auth;
55  import org.kuali.common.dns.api.DnsService;
56  import org.kuali.common.dns.dnsme.DNSMadeEasyDnsService;
57  import org.kuali.common.dns.dnsme.URLS;
58  import org.kuali.common.dns.dnsme.model.DNSMadeEasyServiceContext;
59  import org.kuali.common.dns.model.CNAMEContext;
60  import org.kuali.common.dns.util.CreateOrReplaceCNAME;
61  import org.kuali.common.util.channel.api.ChannelService;
62  import org.kuali.common.util.channel.api.SecureChannel;
63  import org.kuali.common.util.channel.impl.DefaultChannelService;
64  import org.kuali.common.util.channel.model.ChannelContext;
65  import org.kuali.common.util.channel.model.CommandContext;
66  import org.kuali.common.util.channel.model.RemoteFile;
67  import org.kuali.common.util.condition.Condition;
68  import org.kuali.common.util.encrypt.Encryptor;
69  import org.kuali.common.util.file.CanonicalFile;
70  import org.kuali.common.util.log.Loggers;
71  import org.kuali.common.util.maven.RepositoryUtils;
72  import org.kuali.common.util.maven.model.Artifact;
73  import org.kuali.common.util.project.DefaultProjectService;
74  import org.kuali.common.util.project.ProjectService;
75  import org.kuali.common.util.project.model.Project;
76  import org.kuali.common.util.project.model.ProjectIdentifier;
77  import org.kuali.common.util.spring.env.BasicEnvironmentService;
78  import org.kuali.common.util.stream.LoggingStreamConsumer;
79  import org.kuali.common.util.stream.NoOpStreamConsumer;
80  import org.kuali.common.util.wait.DefaultWaitService;
81  import org.kuali.common.util.wait.WaitContext;
82  import org.kuali.common.util.wait.WaitService;
83  import org.slf4j.Logger;
84  
85  import com.amazonaws.regions.Region;
86  import com.amazonaws.regions.Regions;
87  import com.amazonaws.services.ec2.model.Image;
88  import com.amazonaws.services.ec2.model.Instance;
89  import com.amazonaws.services.ec2.model.Tag;
90  import com.google.common.base.Joiner;
91  import com.google.common.base.Splitter;
92  import com.google.common.base.Stopwatch;
93  import com.google.common.collect.ImmutableList;
94  import com.google.common.collect.ImmutableMap;
95  
96  public class SpinUpJenkinsMaster {
97  
98  	private static final Logger logger = Loggers.newLogger();
99  
100 	private final Stopwatch sw = createStarted();
101 	private final List<KualiSecurityGroup> securityGroups = ImmutableList.of(CI.getGroup(), CI_MASTER.getGroup());
102 	private final String amazonAccount = Constants.KUALI_FOUNDATION_ACCOUNT;
103 	private static final String DOMAIN = Constants.DOMAIN;
104 	private final Distro distro = Distro.UBUNTU;
105 	private final String distroVersion = Constants.DISTRO_VERSION;
106 	private static final String ROOT = Constants.ROOT;
107 	private static final String UBUNTU = Constants.UBUNTU;
108 	private final Encryptor encryptor = getDefaultEncryptor();
109 
110 	// What should we go with for default root volume size, 256?)
111 	private static final int DEFAULT_ROOT_VOLUME_SIZE = 256;
112 
113 	@Test
114 	public void test() throws Exception {
115 		VirtualSystem vs = VirtualSystem.create();
116 		Map<String, JenkinsContext> contexts = getJenkinsContexts(Tags.Name.MASTER);
117 		// Default to quiet mode unless they've supplied -Dec2.quiet=false
118 		boolean quiet = equalsIgnoreCase(vs.getProperties().getProperty("ec2.quiet"), "false") ? false : true;
119 		JenkinsContext jenkinsContext = getJenkinsContext(vs, contexts);
120 		String dnsPrefix = jenkinsContext.getDnsPrefix();
121 		String jenkinsMaster = Joiner.on('.').join(dnsPrefix, DOMAIN);
122 		List<Tag> tags = getMasterTags(jenkinsContext, jenkinsMaster);
123 		info("jenkins -> [%s :: %s]", jenkinsContext.getStack().getTag().getValue(), jenkinsMaster);
124 		KeyPair keyPair = CreateBuildSlaveAMI.DEVOPS_KEYPAIR;
125 		String privateKey = keyPair.getPrivateKey();
126 		BasicLaunchRequest request = getMasterLaunchRequest(jenkinsContext);
127 		ProjectIdentifier pid = KUALI_DEVOPS_PROJECT_IDENTIFIER;
128 
129 		EC2Service service = getEC2Service(amazonAccount, jenkinsContext.getRegion());
130 		Instance instance = getInstance(service, request, tags, jenkinsContext);
131 		info("public dns: %s", instance.getPublicDnsName());
132 		updateDns(instance, jenkinsMaster);
133 		String dns = instance.getPublicDnsName();
134 		// While spinning things up, use the Amazon DNS name since the DNSME alias can take a while (few minutes) to propagate
135 		verifySSH(UBUNTU, dns, privateKey);
136 		bootstrap(dns, privateKey);
137 		SecureChannel channel = openSecureChannel(ROOT, dns, privateKey, quiet);
138 		String basedir = publishProject(channel, pid, ROOT, dns, quiet);
139 		String aesPassphrase = encryptor.decrypt(AES_PASSPHRASE_ENCRYPTED);
140 		String quietFlag = (quiet) ? "-q" : "";
141 
142 		// configure basics, java, and tomcat
143 		setupEssentials(channel, basedir, pid, distro, distroVersion, aesPassphrase, dnsPrefix, quietFlag);
144 
145 		// do jenkins specific configuration
146 		String common = getResource(basedir, pid, distro, distroVersion, "jenkins/configurecommon");
147 		String jenkins = getResource(basedir, pid, distro, distroVersion, "jenkins/installjenkins");
148 		String configureMaster = getResource(basedir, pid, distro, distroVersion, "jenkins/configuremaster");
149 		String latestSlaveAMI = findLatestSlaveAMI(service, jenkinsContext.getStack().getTag());
150 		String stack = jenkinsContext.getStack().getTag().getValue();
151 		exec(channel, common, quietFlag, jenkinsMaster, stack, aesPassphrase);
152 		String backupMode = jenkinsContext.getBackupMode().name().toLowerCase();
153 		exec(channel, jenkins, quietFlag, JENKINS_VERSION);
154 		exec(channel, configureMaster, quietFlag, jenkinsMaster, jenkinsContext.getRegion().getName(), stack, backupMode, latestSlaveAMI, JENKINS_VERSION, aesPassphrase);
155 
156 		// The spin up process should have given DNS enough time to settle down
157 		info("Verifying SSH to -> [%s]", jenkinsMaster);
158 		verifySSH(ROOT, jenkinsMaster, privateKey);
159 		info("[%s] jenkins is ready - %s", jenkinsMaster, getTime(sw));
160 	}
161 
162 	protected Instance getInstance(EC2Service service, BasicLaunchRequest request, List<Tag> tags, JenkinsContext jenkinsContext) {
163 		String instanceId = System.getProperty("ec2.instance");
164 		if (instanceId == null) {
165 			logger.info("Launching new instance");
166 			return launchAndWait(service, request, securityGroups, tags, jenkinsContext.getRegion().getName());
167 		} else {
168 			logger.info(format("Using existing instance %s", instanceId));
169 			return service.getInstance(instanceId);
170 		}
171 	}
172 
173 	protected String findLatestSlaveAMI(EC2Service service, Tag stack) {
174 		List<Image> images = service.getMyImages();
175 		List<Image> slaveImages = CreateBuildSlaveAMI.getFilteredImages(images, stack, CreateBuildSlaveAMI.name.getKey(), CI_SLAVE_STARTS_WITH_TOKEN);
176 		// Sort by Name tag
177 		sort(slaveImages, new ImageTagsComparator());
178 
179 		// Most recent images are at the bottom (we need them at the top)
180 		reverse(slaveImages);
181 		checkState(slaveImages.size() > 0, "expected at least one slave image but there were zero");
182 		return slaveImages.get(0).getImageId();
183 	}
184 
185 	protected static Map<String, JenkinsContext> getJenkinsContexts(Tags.Name name) {
186 		String region = System.getProperty("ec2.region", Regions.DEFAULT_REGION.getName());
187 		JenkinsContext prod = JenkinsContext.builder().withRegion(region).withDnsPrefix("ci").withStack(Tags.Stack.PROD).withName(name).build();
188 		JenkinsContext test = JenkinsContext.builder().withRegion(region).withDnsPrefix("testci").withStack(Tags.Stack.TEST).withName(name).build();
189 		SortedMap<String, JenkinsContext> contexts = newTreeMap();
190 		contexts.put("test", test);
191 		contexts.put("prod", prod);
192 		return ImmutableMap.copyOf(contexts);
193 	}
194 
195 	protected static JenkinsContext getJenkinsContext(VirtualSystem vs, Map<String, JenkinsContext> contexts) {
196 		String usage = format("\n\nusage: -Dec2.stack=%s\n\n", Joiner.on('/').join(contexts.keySet()));
197 		String jenkinsContextKey = vs.getProperties().getProperty("ec2.stack");
198 		checkState(jenkinsContextKey != null, usage);
199 		JenkinsContext jenkinsContext = contexts.get(jenkinsContextKey);
200 		checkState(jenkinsContext != null, usage);
201 		return jenkinsContext;
202 	}
203 
204 	protected static void setupEssentials(SecureChannel channel, String basedir, ProjectIdentifier pid, Distro distro, String distroVersion, String aesPassphrase,
205 			String dnsPrefix, String quietFlag) {
206 		String basics = getResource(basedir, pid, distro, distroVersion, "common/configurebasics");
207 		String ssd = getResource(basedir, pid, distro, distroVersion, "common/configuressd");
208 		String sethostname = getResource(basedir, pid, distro, distroVersion, "common/sethostname");
209 		String java = getResource(basedir, pid, distro, distroVersion, "common/installjava");
210 		String tomcat = getResource(basedir, pid, distro, distroVersion, "common/installtomcat");
211 		exec(channel, basics, quietFlag);
212 		exec(channel, ssd, quietFlag);
213 		exec(channel, sethostname, dnsPrefix, DOMAIN);
214 		exec(channel, java, quietFlag, "jdk6", System.getProperty("jdk6.version", JDK6_VERSION), aesPassphrase);
215 		exec(channel, java, quietFlag, "jdk7", System.getProperty("jdk7.version", JDK7_VERSION), aesPassphrase);
216 		exec(channel, java, quietFlag, "jdk8", System.getProperty("jdk8.version", JDK8_VERSION), aesPassphrase);
217 		exec(channel, tomcat, quietFlag, "tomcat7", "jdk7", aesPassphrase);
218 	}
219 
220 	protected static void bootstrap(String hostname, String privateKey) throws IOException {
221 		info("[%s] enabling root ssh", hostname);
222 		enableRootSSH(UBUNTU, hostname, privateKey);
223 	}
224 
225 	protected static String publishProject(SecureChannel channel, ProjectIdentifier pid, String username, String hostname, boolean quiet) {
226 		ProjectService service = new DefaultProjectService(new BasicEnvironmentService());
227 		Project project = service.getProject(pid);
228 		info(project.getArtifactId());
229 		Artifact artifact = Artifact.builder(project.getGroupId(), project.getArtifactId(), project.getVersion()).type("tar.gz").build();
230 		File repo = getDefaultLocalRepository();
231 		CanonicalFile jar = new CanonicalFile(RepositoryUtils.getFile(repo, artifact));
232 		String remoteBasedir = "/root/.bootstrap";
233 		String tarGzFile = project.getArtifactId() + ".tar.gz";
234 		String remotePublishDir = format("%s/%s", remoteBasedir, project.getArtifactId());
235 		RemoteFile remoteJar = new RemoteFile.Builder(format("%s/%s", remoteBasedir, tarGzFile)).build();
236 		String to = username + "@" + hostname + ":" + remoteJar.getAbsolutePath();
237 		info("scp:from -> %s", jar);
238 		info("scp:to   -> %s", to);
239 		channel.scp(jar, remoteJar);
240 		info("update  -> package indexes");
241 		execFormattedCommand(channel, quiet, "apt-get update -y");
242 		// Attempting to install the unzip package failed 2 nights in a row with error code 100
243 		// Trying a 3 second delay here just to see if that helps
244 		// info("sleep  -> 3 seconds");
245 		// Threads.sleep(3000);
246 		// info("install  -> unzip");
247 		// execFormattedCommand(channel, quiet, "apt-get install unzip -y");
248 		info("purge    -> %s", remotePublishDir);
249 		execFormattedCommand(channel, quiet, "rm -rf %s", remotePublishDir);
250 		info("create   -> %s", remotePublishDir);
251 		// execFormattedCommand(channel, quiet, "unzip -o %s -d %s", remoteJar.getAbsolutePath(), remotePublishDir);
252 		execFormattedCommand(channel, quiet, "mkdir -p %s", remotePublishDir);
253 		info("unpack   -> %s to %s", remoteJar.getAbsolutePath(), remotePublishDir);
254 		execFormattedCommand(channel, quiet, "tar -xvf %s -C %s", remoteJar.getAbsolutePath(), remotePublishDir);
255 		execFormattedCommand(channel, quiet, "chmod -R 755 %s", remotePublishDir);
256 		return remotePublishDir;
257 	}
258 
259 	protected static ChannelContext.Builder getSilentContextBuilder(String hostname) {
260 		ChannelContext.Builder builder = new ChannelContext.Builder(hostname);
261 		builder.echo(false);
262 		builder.debug(false);
263 		return builder;
264 	}
265 
266 	protected static String getResource(String basedir, ProjectIdentifier project, Distro distro, String version, String script) {
267 		List<String> tokens = newArrayList();
268 		tokens.add(basedir);
269 		tokens.addAll(Splitter.on('.').splitToList(project.getGroupId()));
270 		tokens.add(project.getArtifactId());
271 		tokens.add(distro.getName());
272 		tokens.add(version);
273 		tokens.add(script);
274 		return Joiner.on('/').join(tokens);
275 	}
276 
277 	protected static void exec(SecureChannel channel, String command, String arg) {
278 		exec(channel, command, singletonList(arg));
279 	}
280 
281 	protected static void exec(SecureChannel channel, String command, String... args) {
282 		exec(channel, command, ImmutableList.copyOf(args));
283 	}
284 
285 	protected static void exec(SecureChannel channel, String command, List<String> args) {
286 		StreamConsumer stdout = new LoggingStreamConsumer(logger, INFO);
287 		StreamConsumer stderr = new LoggingStreamConsumer(logger, WARN);
288 		String cmd = command + " " + Joiner.on(' ').join(args);
289 		CommandContext context = new CommandContext.Builder(cmd).stdout(stdout).stderr(stderr).build();
290 		channel.exec(context);
291 	}
292 
293 	protected static void execFormattedCommand(SecureChannel channel, boolean quiet, String command, Object... args) {
294 		StreamConsumer stdout = (quiet) ? NoOpStreamConsumer.INSTANCE : new LoggingStreamConsumer(logger, INFO);
295 		StreamConsumer stderr = (quiet) ? NoOpStreamConsumer.INSTANCE : new LoggingStreamConsumer(logger, WARN);
296 		String formatted = formatString(command, args);
297 		CommandContext context = new CommandContext.Builder(formatted).stdout(stdout).stderr(stderr).build();
298 		channel.exec(context);
299 	}
300 
301 	protected static SecureChannel openSecureChannel(String username, String hostname, String privateKey, boolean quiet) throws IOException {
302 		ChannelContext context = getSilentContextBuilder(hostname).echo(!quiet).requestPseudoTerminal(true).debug(!quiet).username(username).privateKey(privateKey)
303 				.connectTimeout(getMillisAsInt("30s")).build();
304 		ChannelService service = new DefaultChannelService();
305 		return service.openChannel(context);
306 	}
307 
308 	protected static void enableRootSSH(String username, String hostname, String privateKey) throws IOException {
309 		SecureChannel channel = openSecureChannel(username, hostname, privateKey, true);
310 		execFormattedCommand(channel, true, "sudo cp /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keys");
311 	}
312 
313 	protected static void verifySSH(String username, String hostname, String privateKey) {
314 		WaitContext context = WaitContext.builder(getMillisAsInt("30m")).sleepMillis(getMillisAsInt("5s")).build();
315 		WaitService service = new DefaultWaitService();
316 		Condition condition = getSshCondition(username, hostname, privateKey);
317 		service.wait(context, condition);
318 	}
319 
320 	protected static Condition getSshCondition(String username, String hostname, String privateKey) {
321 		ChannelContext context = new ChannelContext.Builder(hostname).username(username).privateKey(privateKey).connectTimeout(getMillisAsInt("5s")).build();
322 		ChannelService service = new DefaultChannelService();
323 		return new VerifiedSSHCondition(service, context);
324 	}
325 
326 	protected void updateDns(Instance instance, String alias) {
327 		String canonical = instance.getPublicDnsName();
328 		CNAMEContext context = newCNAMEContext(alias, canonical);
329 		DnsService service = getDnsService();
330 		new CreateOrReplaceCNAME(service, context).execute();
331 	}
332 
333 	protected DnsService getDnsService() {
334 		DNSMadeEasyServiceContext context = new DNSMadeEasyServiceContext(Auth.getDNSMECredentials(), URLS.PRODUCTION, DOMAIN);
335 		return new DNSMadeEasyDnsService(context);
336 	}
337 
338 	protected static BasicLaunchRequest getMasterLaunchRequest(JenkinsContext context) {
339 		BasicLaunchRequest.Builder builder = BasicLaunchRequest.builder();
340 		builder.setTimeoutMillis(getMillisAsInt("30m"));
341 		builder.setAmi(getDefaultAMI(context.getRegion()));
342 		builder.setRootVolume(RootVolume.create(DEFAULT_ROOT_VOLUME_SIZE, true));
343 		return getBasicLaunchRequest(builder.build());
344 	}
345 
346 	protected static List<Tag> getMasterTags(JenkinsContext context, String fqdn) {
347 		List<Tag> tags = newArrayList();
348 		tags.add(new Tag("fqdn", fqdn));
349 		tags.addAll(getCommonTags(context.getStack().getTag()));
350 		tags.add(context.getName().getTag());
351 		return ImmutableList.copyOf(tags);
352 	}
353 
354 	protected static void info(String msg, Object... args) {
355 		logger.info(formatString(msg, args));
356 	}
357 
358 	protected static String formatString(String s, Object... args) {
359 		if (args != null && args.length > 0) {
360 			return format(s, args);
361 		} else {
362 			return s;
363 		}
364 	}
365 
366 	protected static String getDefaultAMI(Region provided) {
367 		Regions derived = Regions.fromName(provided.getName());
368 		switch (derived) {
369 		case US_EAST_1:
370 			return AMI.UBUNTU_64_BIT_PRECISE_LTS_1204_US_EAST_1.getId();
371 		case US_WEST_1:
372 			return AMI.UBUNTU_64_BIT_PRECISE_LTS_1204_US_WEST_1.getId();
373 		case US_WEST_2:
374 			return AMI.UBUNTU_64_BIT_PRECISE_LTS_1204_US_WEST_2.getId();
375 		default:
376 			throw illegalArgument("Region [%s] is not supported", provided.getName());
377 		}
378 	}
379 
380 }
381 // tar -xvf /root/.bootstrap/kuali-devops-bin.tar.gz -C /root/.bootstrap/kuali-devops