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
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
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
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
143 setupEssentials(channel, basedir, pid, distro, distroVersion, aesPassphrase, dnsPrefix, quietFlag);
144
145
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
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
177 sort(slaveImages, new ImageTagsComparator());
178
179
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
243
244
245
246
247
248 info("purge -> %s", remotePublishDir);
249 execFormattedCommand(channel, quiet, "rm -rf %s", remotePublishDir);
250 info("create -> %s", remotePublishDir);
251
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