View Javadoc
1   package org.kuali.common.devops.ubuntu;
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.Iterables.filter;
6   import static com.google.common.collect.Lists.newArrayList;
7   import static java.lang.String.format;
8   import static org.kuali.common.core.collect.Iterables.getSingleElement;
9   import static org.kuali.common.devops.logic.DNS.getCanonicalMap;
10  import static org.kuali.common.util.FormatUtils.getTime;
11  import static org.kuali.common.util.base.Exceptions.illegalState;
12  import static org.kuali.common.util.encrypt.Encryption.getDefaultEncryptor;
13  import static org.kuali.common.util.log.Loggers.newLogger;
14  import static org.springframework.util.CollectionUtils.isEmpty;
15  
16  import java.io.IOException;
17  import java.util.List;
18  import java.util.Map;
19  
20  import org.codehaus.plexus.util.cli.StreamConsumer;
21  import org.kuali.common.aws.ec2.api.EC2Service;
22  import org.kuali.common.aws.ec2.model.VolumeRequest;
23  import org.kuali.common.core.base.TimedInterval;
24  import org.kuali.common.core.ssh.KeyPair;
25  import org.kuali.common.util.channel.api.SecureChannel;
26  import org.kuali.common.util.channel.model.CommandContext;
27  import org.kuali.common.util.channel.model.CommandResult;
28  import org.kuali.common.util.encrypt.Encryptor;
29  import org.kuali.common.util.stream.LoggingStreamConsumer;
30  import org.slf4j.Logger;
31  
32  import com.amazonaws.services.ec2.model.Instance;
33  import com.amazonaws.services.ec2.model.Tag;
34  import com.amazonaws.services.ec2.model.Volume;
35  import com.amazonaws.services.ec2.model.VolumeAttachment;
36  import com.google.common.base.Joiner;
37  import com.google.common.base.Predicate;
38  import com.google.common.base.Stopwatch;
39  import com.google.common.collect.ImmutableMap;
40  
41  public class VolumeMounter {
42  
43  	private static final Logger logger = newLogger();
44  
45  	private final ImmutableMap<String, String> dns = ImmutableMap.copyOf(getCanonicalMap(true));
46  
47  	public TimedInterval mount(EC2Service ec2, MountVolumeRequest avr) {
48  		try {
49  			Stopwatch sw = createStarted();
50  			List<Volume> volumes = ec2.listVolumes();
51  			Predicate<Volume> predicate = new MatchAllTagsPredicate(avr.getVolumeTags());
52  			Volume volume = getSingleElement(filter(volumes, predicate));
53  			String volumeId = volume.getVolumeId();
54  			Instance instance = ec2.getInstance(avr.getInstanceId());
55  			info("attach  -> %s [%s]", volumeId, asDisplayString(avr.getVolumeTags()));
56  			info("to      -> %s:%s", getHostname(instance, dns), avr.getDevice());
57  			if (isDetach(volume, avr.getInstanceId(), avr.getDevice())) {
58  				// We only use "normal" file systems where volumes are attached to at most one instance
59  				VolumeAttachment attachment = getSingleElement(volume.getAttachments());
60  				String device = attachment.getDevice();
61  				Instance detachFrom = ec2.getInstance(attachment.getInstanceId());
62  				// display the hostname, but always open the channel to the public dns name
63  				String hostname = getHostname(detachFrom, dns);
64  				info("detach  -> %s:%s:%s", hostname, detachFrom.getInstanceId(), device);
65  				SecureChannel channel = openChannel(detachFrom.getPublicDnsName());
66  				unmount(channel, device);
67  				VolumeRequest request = VolumeRequest.builder().withInstanceId(detachFrom.getInstanceId()).withVolumeId(volumeId).withDevice(device).build();
68  				ec2.detachVolume(request);
69  			}
70  			if (!isAlreadyAttached(volume, avr.getInstanceId(), avr.getDevice())) {
71  				info("attach  -> %s:%s:%s", getHostname(instance, dns), instance.getInstanceId(), avr.getDevice());
72  				VolumeRequest request = VolumeRequest.builder().withInstanceId(avr.getInstanceId()).withVolumeId(volumeId).withDevice(avr.getDevice()).build();
73  				ec2.attachVolume(request);
74  			} else {
75  				info("volume  -> already attached");
76  			}
77  			// always open the channel to amazon's public dns name, not the alias
78  			SecureChannel channel = openChannel(instance.getPublicDnsName());
79  			mount(channel, avr);
80  			Volume attached = ec2.getVolume(volume.getVolumeId());
81  			checkAttached(attached, avr.getInstanceId(), avr.getDevice());
82  			info("elapsed -> %s", getTime(sw));
83  			return TimedInterval.build(sw);
84  		} catch (IOException e) {
85  			throw illegalState(e);
86  		}
87  	}
88  
89  	private String getHostname(Instance instance, Map<String, String> dns) {
90  		String alias = dns.get(instance.getPublicDnsName());
91  		if (alias == null) {
92  			return instance.getPublicDnsName();
93  		} else {
94  			return alias;
95  		}
96  	}
97  
98  	private String asDisplayString(List<Tag> tags) {
99  		List<String> lines = newArrayList();
100 		for (Tag tag : tags) {
101 			lines.add(tag.getKey() + " -> " + tag.getValue());
102 		}
103 		return Joiner.on(", ").join(lines);
104 	}
105 
106 	private String checkAttached(Volume volume, String instanceId, String device) {
107 		// Extract the instance it's attached to
108 		VolumeAttachment attachment = getSingleElement(volume.getAttachments());
109 		checkState(attachment.getInstanceId().equals(instanceId), "must be attached to [%s] not [%s]", instanceId, attachment.getInstanceId());
110 		checkState(attachment.getDevice().equals(device), "must be attached as '%s' not '%s'", device, attachment.getDevice());
111 		return volume.getVolumeId();
112 	}
113 
114 	private boolean isAlreadyAttached(Volume volume, String instanceId, String device) {
115 
116 		if (isEmpty(volume.getAttachments())) {
117 			return false;
118 		}
119 
120 		// Extract the instance it's attached to
121 		VolumeAttachment attachment = getSingleElement(volume.getAttachments());
122 
123 		if (!attachment.getInstanceId().equals(instanceId)) {
124 			return false;
125 		}
126 
127 		if (!attachment.getDevice().equals(device)) {
128 			return false;
129 		}
130 
131 		return true;
132 	}
133 
134 	private boolean isDetach(Volume volume, String instanceId, String device) {
135 
136 		// It's not currently attached to anything, we are done
137 		if (isEmpty(volume.getAttachments())) {
138 			return false;
139 		}
140 
141 		// Extract the instance it's attached to
142 		VolumeAttachment attachment = getSingleElement(volume.getAttachments());
143 
144 		// If it's a different instance we must first detach it
145 		if (!attachment.getInstanceId().equals(instanceId)) {
146 			return true;
147 		}
148 
149 		// If the device name is different, we must detach then re-attach using the right device name
150 		if (!attachment.getDevice().equals(device)) {
151 			return true;
152 		}
153 
154 		// If we get here, it's attached to the right instance, using the right device name
155 		return false;
156 	}
157 
158 	private static void unmount(SecureChannel channel, String device) {
159 		if (isMounted(channel, device)) {
160 			channel.exec(format("umount -f %s", device));
161 		}
162 	}
163 
164 	private static void mount(SecureChannel channel, MountVolumeRequest request) {
165 		if (!isMounted(channel, request.getDevice())) {
166 			channel.exec(format("mkdir -p %s", request.getPath()));
167 			if (request.getOwnership().isPresent()) {
168 				channel.exec(format("chown %s %s", request.getOwnership().get(), request.getPath()));
169 			}
170 			String command = format("mount %s %s", request.getDevice(), request.getPath());
171 			CommandContext.Builder builder = CommandContext.builder(command);
172 			StreamConsumer sc = new LoggingStreamConsumer(logger);
173 			builder.stderr(sc);
174 			builder.stdout(sc);
175 			CommandContext cc = builder.build();
176 			channel.exec(cc);
177 		}
178 	}
179 
180 	private static boolean isMounted(SecureChannel channel, String device) {
181 		String command = format("mount | grep %s", device);
182 		CommandContext cc = CommandContext.builder(command).ignoreExitValue(true).build();
183 		CommandResult result = channel.exec(cc);
184 		int exitValue = result.getExitValue();
185 		return exitValue == 0;
186 	}
187 
188 	@Deprecated
189 	private static final SecureChannel openChannel(String hostname) throws IOException {
190 		org.kuali.common.util.channel.model.ChannelContext.Builder builder = org.kuali.common.util.channel.model.ChannelContext.builder(hostname);
191 		builder.privateKey(getKualiDevopsPrivateKey());
192 		builder.username("root");
193 		builder.requestPseudoTerminal(true);
194 		org.kuali.common.util.channel.model.ChannelContext ctx = builder.build();
195 		org.kuali.common.util.channel.api.ChannelService cs = new org.kuali.common.util.channel.impl.DefaultChannelService();
196 		return cs.openChannel(ctx);
197 	}
198 
199 	protected static void debug(String msg, Object... args) {
200 		logger.debug((args == null || args.length == 0) ? msg : format(msg, args));
201 	}
202 
203 	protected static void info(String msg, Object... args) {
204 		logger.info((args == null || args.length == 0) ? msg : format(msg, args));
205 	}
206 
207 	private static String getKualiDevopsPrivateKey() {
208 		Encryptor enc = getDefaultEncryptor();
209 		String publicKey = enc
210 				.decrypt("U2FsdGVkX1/mOVOU9KiA43fWz10qtG08ixrGGBk+DsuFEikZ1N3bz5BUiPD4G7okAeQgen+RQ4E40/AybeLRjJUuOgLUdeOCpcK6u/H+EZs+N820xZe1Hpq/wchfs13npj6gWpOWylQAhl5DSioe5wHxkQC3hORV13sr5Mhaf+SorF/NPyzvERsJZ/FoScV+RcGxLfwVmlAUEVdsYNfgUepXPUWwTDmNu4FcRO/8ud7Kth7E3BkwG7sTN9n7pkEbA632ZdjcccBIptOfurArMLORN8VsCbiQU8uFo2leudzsa5zdx6JsxiGHHQeybogZ95lLwxAl48ka4YfpyVDaDA7Bf3nUp2xMbqvTaoPYMXrp8Y0Qk/aDhsDIhfSd72Sk/XE/RI/sjlzO2Owp0wp9Wb9uT3AsSAu44qjqCr8/AY9BUTn/SSc/+2COnO7YUgMSbo/fy3WK2twIyTd/YiF+qjaNjLUdYwxZDiD0s/g0pk46rnKdTy406KRO5W4uGIvVO5WpLsMk3chLctas3KJYreZ1pUV4gnnC8R2baqRzkgg=");
211 		String privateKey = enc
212 				.decrypt("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");
213 		return KeyPair.builder("kuali-devops").withPublicKey(publicKey).withPrivateKey(privateKey).build().getPrivateKey();
214 	}
215 
216 }