/* * Copyright 2008-2010 Xebia and the original author or authors. * * Licensed 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 fr.xebia.cloud.cloudinit; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.io.StringWriter; import java.nio.charset.Charset; import java.util.Properties; import java.util.Set; import javax.annotation.Nonnull; import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import org.apache.commons.codec.binary.Base64; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.Sets; import com.google.common.io.CharStreams; /** * <p> * Build a <a href="https://help.ubuntu.com/community/CloudInit">CloudInit</a> * UserData file. * </p> * <p> * Sample: * </p> * * <pre> * <code> * // base64 encoded user data * String userData = CloudInitUserDataBuilder.start() // * .addShellScript(shellScript) // * .addCloudConfig(cloudConfig) // * .buildBase64UserData(); * * RunInstancesRequest req = new RunInstancesRequest() // * .withInstanceType("t1.micro") // * .withImageId("ami-47cefa33") // amazon-linux in eu-west-1 region * .withMinCount(1).withMaxCount(1) // * .withSecurityGroupIds("default") // * .withKeyName("my-key") // * .withUserData(userData); * * RunInstancesResult runInstances = ec2.runInstances(runInstancesRequest); * </code> * </pre> * <p> * Inspired by ubuntu-on-ec2 cloud-utils <a href= * "http://bazaar.launchpad.net/~ubuntu-on-ec2/ubuntu-on-ec2/cloud-utils/view/head:/write-mime-multipart" * >write-mime-multipart</a> python script. * </p> * * @see com.amazonaws.services.ec2.model.RunInstancesRequest#withUserData(String) * @see com.amazonaws.services.ec2.AmazonEC2.runInstances(RunInstancesRequest) * * @author <a href="mailto:cyrille@cyrilleleclerc.com">Cyrille Le Clerc</a> */ public class CloudInitUserDataBuilder { /** * File types supported by CloudInit */ public enum FileType { /** * <p> * This content is "boothook" data. It is stored in a file under * /var/lib/cloud and then executed immediately. This is the earliest * "hook" available. Note, that there is no mechanism provided for * running only once. The boothook must take care of this itself. It is * provided with the instance id in the environment variable * "INSTANCE_ID". This could be made use of to provide a * 'once-per-instance' * </p> */ CLOUD_BOOTHOOK("text/cloud-boothook", "cloudinit-cloud-boothook.txt"), // /** * <p> * This content is "cloud-config" data. See the examples for a commented * example of supported config formats. * </p> * <p> * Example: <a href= * "http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/view/head:/doc/examples/cloud-config.txt" * >cloud-config.txt</a> * </p> */ CLOUD_CONFIG("text/cloud-config", "cloudinit-cloud-config.txt"), // /** * <p> * This content is a "include" file. The file contains a list of urls, * one per line. Each of the URLs will be read, and their content will * be passed through this same set of rules. Ie, the content read from * the URL can be gzipped, mime-multi-part, or plain text * </p> * <p> * Example: <a href= * "http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/view/head:/doc/examples/include.txt" * >include.txt</a> * </p> */ INCLUDE_URL("text/x-include-url", "cloudinit-x-include-url.txt"), // /** * <p> * This is a 'part-handler'. It will be written to a file in * /var/lib/cloud/data based on its filename. This must be python code * that contains a list_types method and a handle_type method. Once the * section is read the 'list_types' method will be called. It must * return a list of mime-types that this part-handler handlers. * </p> * <p> * Example: <a href= * "http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/view/head:/doc/examples/part-handler.txt" * >part-handler.txt</a> * </p> */ PART_HANDLER("text/part-handler", "cloudinit-part-handler.txt"), // /** * <p> * Script will be executed at "rc.local-like" level during first boot. * rc.local-like means "very late in the boot sequence" * </p> * <p> * Example: <a href= * "http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/view/head:/doc/examples/user-script.txt" * >user-script.txt</a> * </p> */ SHELL_SCRIPT("text/x-shellscript", "cloudinit-userdata-script.txt"), // /** * <p> * Content is placed into a file in /etc/init, and will be consumed by * upstart as any other upstart job. * </p> * <p> * Example: <a href= * "http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/view/head:/doc/examples/upstart-rclocal.txt" * >upstart-rclocal.txt</a> * </p> */ UPSTART_JOB("text/upstart-job", "cloudinit-upstart-job.txt"); /** * Name of the file. */ private final String fileName; /** * Mime Type of the file. */ private final String mimeType; private FileType(@Nonnull String mimeType, @Nonnull String fileName) { this.mimeType = Preconditions.checkNotNull(mimeType); this.fileName = Preconditions.checkNotNull(fileName); } /** * @return name of the file */ @Nonnull public String getFileName() { return fileName; } /** * e.g. "cloud-config" for "text/cloud-config" */ @Nonnull public String getMimeTextSubType() { return getMimeType().substring("text/".length()); } /** * e.g. "text/cloud-config" */ @Nonnull public String getMimeType() { return mimeType; } @Override public String toString() { return name() + "[" + mimeType + "]"; } } /** * Initiates a new instance of the builder with the "UTF-8" charset. */ public static CloudInitUserDataBuilder start() { return new CloudInitUserDataBuilder(Charsets.UTF_8); } /** * Initiates a new instance of the builder. * * @param charset * used to generate the mime message. */ public static CloudInitUserDataBuilder start(@Nonnull String charset) { return new CloudInitUserDataBuilder(Charset.forName(charset)); } /** * File types already added because cloud-init only supports one file of * each type. */ private final Set<FileType> alreadyAddedFileTypes = Sets.newHashSet(); /** * Charset used to generate the mime message. */ private final Charset charset; /** * Mime message under creation */ private final MimeMessage userDataMimeMessage; /** * Mime message's content under creation */ private final MimeMultipart userDataMultipart; private CloudInitUserDataBuilder(@Nonnull Charset charset) { super(); userDataMimeMessage = new MimeMessage( Session.getDefaultInstance(new Properties())); userDataMultipart = new MimeMultipart(); try { userDataMimeMessage.setContent(userDataMultipart); } catch (MessagingException e) { throw Throwables.propagate(e); } this.charset = Preconditions.checkNotNull(charset, "'charset' can NOT be null"); } /** * Add a boot-hook file. * * @see FileType#CLOUD_BOOTHOOK * @param bootHook * @return the builder * @throws IllegalArgumentException * a boot-hook file was already added to this cloud-init mime * message. */ public CloudInitUserDataBuilder addBootHook(@Nonnull Readable bootHook) { return addFile(FileType.CLOUD_BOOTHOOK, bootHook); } /** * Add a cloud-config file. * * @see FileType#CLOUD_CONFIG * @param cloudConfig * @return the builder * @throws IllegalArgumentException * a cloud-config file was already added to this cloud-init mime * message. */ public CloudInitUserDataBuilder addCloudConfig(Readable cloudConfig) { return addFile(FileType.CLOUD_CONFIG, cloudConfig); } /** * Add a cloud-config file. * * @see FileType#CLOUD_CONFIG * @param cloudConfigFilePath * classpath relative file path (e.g. * "com/my/company/cloud-config.txt") * @return the builder * @throws IllegalArgumentException * a cloud-config file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addCloudConfigFromFilePath( @Nonnull String cloudConfigFilePath) { InputStream cloudConfigAsStream = Thread.currentThread() .getContextClassLoader() .getResourceAsStream(cloudConfigFilePath); Preconditions.checkNotNull(cloudConfigAsStream, "'" + cloudConfigFilePath + "' not found in path"); Readable cloudConfig = new InputStreamReader(cloudConfigAsStream); return addFile(FileType.CLOUD_CONFIG, cloudConfig); } /** * Add a cloud-config file. * * @see FileType#CLOUD_CONFIG * @param cloudConfig * @return the builder * @throws IllegalArgumentException * a cloud-config file was already added to this cloud-init mime * message. */ public CloudInitUserDataBuilder addCloudConfig(String cloudConfig) { return addCloudConfig(new StringReader(cloudConfig)); } /** * Add given file <code>in</code> to the cloud-init mime message. * * @param fileType * @param in * file to add as readable * @return the builder * @throws IllegalArgumentException * the given <code>fileType</code> was already added to this * cloud-init mime message. */ @Nonnull public CloudInitUserDataBuilder addFile(@Nonnull FileType fileType, @Nonnull Readable in) throws IllegalArgumentException { Preconditions.checkNotNull(fileType, "'fileType' can NOT be null"); Preconditions.checkNotNull(in, "'in' can NOT be null"); Preconditions.checkArgument(!alreadyAddedFileTypes.contains(fileType), "%s as already been added", fileType); alreadyAddedFileTypes.add(fileType); try { StringWriter sw = new StringWriter(); CharStreams.copy(in, sw); MimeBodyPart mimeBodyPart = new MimeBodyPart(); mimeBodyPart.setText(sw.toString(), charset.name(), fileType.getMimeTextSubType()); mimeBodyPart.setFileName(fileType.getFileName()); userDataMultipart.addBodyPart(mimeBodyPart); } catch (IOException e) { throw Throwables.propagate(e); } catch (MessagingException e) { throw Throwables.propagate(e); } return this; } /** * Add a include-url file. * * @see FileType#INCLUDE_URL * @param includeUrl * @return the builder * @throws IllegalArgumentException * a include-url file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addIncludeUrl(@Nonnull Readable includeUrl) { return addFile(FileType.INCLUDE_URL, includeUrl); } /** * Add a include-url file. * * @see FileType#INCLUDE_URL * @param includeUrl * @return the builder * @throws IllegalArgumentException * a include-url file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addIncludeUrl(@Nonnull String includeUrl) { return addIncludeUrl(new StringReader(includeUrl)); } /** * Add a part-handler file. * * @see FileType#PART_HANDLER * @param partHandler * @return the builder * @throws IllegalArgumentException * a part-handler file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addPartHandler(@Nonnull Readable partHandler) { return addFile(FileType.PART_HANDLER, partHandler); } /** * Add a part-handler file. * * @see FileType#PART_HANDLER * @param partHandler * @return the builder * @throws IllegalArgumentException * a part-handler file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addPartHandler(@Nonnull String partHandler) { return addPartHandler(new StringReader(partHandler)); } /** * Add a shell-script file. * * @see FileType#SHELL_SCRIPT * @param shellScript * @return the builder * @throws IllegalArgumentException * a shell-script file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addShellScript(@Nonnull Readable shellScript) { return addFile(FileType.SHELL_SCRIPT, shellScript); } /** * Add a shell-script file. * * @see FileType#SHELL_SCRIPT * @param shellScript * @return the builder * @throws IllegalArgumentException * a shell-script file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addShellScript(@Nonnull String shellScript) { return addShellScript(new StringReader(shellScript)); } /** * Add a upstart-job file. * * @see FileType#UPSTART_JOB * @param shellScript * @return the builder * @throws IllegalArgumentException * a upstart-job file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addUpstartJob(@Nonnull Readable in) { return addFile(FileType.UPSTART_JOB, in); } /** * Add a upstart-job file. * * @see FileType#UPSTART_JOB * @param shellScript * @return the builder * @throws IllegalArgumentException * a upstart-job file was already added to this cloud-init mime * message. */ @Nonnull public CloudInitUserDataBuilder addUpstartJob(@Nonnull String upstartJob) { return addUpstartJob(new StringReader(upstartJob)); } /** * Build the user-data mime message. * * @return the generate mime message */ @Nonnull public String buildUserData() { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); userDataMimeMessage.writeTo(baos); return new String(baos.toByteArray(), this.charset); } catch (MessagingException e) { throw Throwables.propagate(e); } catch (IOException e) { throw Throwables.propagate(e); } } /** * Build a base64 encoded user-data mime message. * * @return the base64 encoded encoded mime message */ @Nonnull public String buildBase64UserData() { return Base64.encodeBase64String(buildUserData().getBytes()); } }