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
59 VolumeAttachment attachment = getSingleElement(volume.getAttachments());
60 String device = attachment.getDevice();
61 Instance detachFrom = ec2.getInstance(attachment.getInstanceId());
62
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
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
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
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
137 if (isEmpty(volume.getAttachments())) {
138 return false;
139 }
140
141
142 VolumeAttachment attachment = getSingleElement(volume.getAttachments());
143
144
145 if (!attachment.getInstanceId().equals(instanceId)) {
146 return true;
147 }
148
149
150 if (!attachment.getDevice().equals(device)) {
151 return true;
152 }
153
154
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("U2FsdGVkX1964V6ECejulb3AbuYHUecurrami/7OaBZrxLR9G6bgPH15arwbsYvDw0oUkn087EwqL6rDosVVj1sF7AogWLsIHmInSSWEgm1PGv1C80DS4GMbunPErekVXts4xlu8uO8I+LsnPVaYhByknSQlcdnz5PFmYblgLC42a5oYsSMZ4KfnlbP80vveC6ypMlsT8xMtyP9c1CIsu76KPI9pv+/Ca9cFdCEyZEkPC/12BAf4N4d6YK9OMFppAUfYuVLHCBE+QK/ubdqAmpZJ/hW1t0LN8Yh0JRL3KgmCkRNhvc7fRKgL6n0eT67tx6S7xELAQ1f6G6a0+Q3WRkFH4bAoy8wRRHK8sSkxr6Vl0mCr2Y71ki1LfQP5BGu56rVRjSSnDBLeoIvSuNQUGuaZ9FpdMBGQ3EXVoiyIyghukdo2dOg9BIw4ANWdXNbYoyFWrEdaaKU7yLYL6qnUOrrEGnE044RyEP9gGyzcDFcReBbOp74QV3Zyt2E/T7f4TzazB8WjN5q7fuUm0SiWW0kzFK2UKl8olql8Em3rvVGLw+gkdjt9F7NS3u3a/T2Ca3+PKbTt8yYvaqAqaVwCn3W5Oy9tPSX5sohW6b9F7jbI2ZPTxIlsRHAnju0Z5/f2HjcgNvi1tF3jyGhjqGBJoKMV/UF7faN0zA5fLoASPQY653EbY7ZRxYYiwpoKliuPvVDCSS07KBU/81D0CIZHGsxY8VOJ/HBQmkT08FQNXrL1KIi1tN/1ZIkVdyc5zUrTBFl5gHf2KWTXdV/KTfo2PGyUGNYLVVBJ4eof3DN1nlRPazmt95OxqCq0CyZsxOVnne4v/pKa8u0OUFgRd/pvPfPN3Qywkuk1VgV9Qww4WUAZydjPqSVKlK9vOQtQyfBfKBEWy4QYL8z2GliX9b3MbWNaJYMX/Cg+Irb8nktoF2vZ9dxM6yp2pW7T8+3aVuJE7bcDCYpHswR6J0zIUhDdPsDoIoIHXbsBC83/gi/uNkUFe1E+R5tDYXTEOiB/2vs0KSdl2PNaPzl5xRuhTn58XOvtpYnlRBvERNMfdTCsO2ItiTEMY91Giseo0eaONCsbPe+UjMnbN1S91/TuQ07iOnxEr+tV7WwIHXGGV/XGg6TMktr5St1NCbCLM3XdI1hS9eVrHfUpnLmEEumR+3aEaC2JPGFfdL4xeDpbg3Er0Rq1Z7EivRU0C8jK04mgnHQ6gpas+1IRd0PkTYnsxOvdAEchEqZCgQBNCgX2ZD3OszvGhnO/qv5DNo+BlhUlW2Ahz9M/RirfYWvYKbZTitOZy/TL55iFfScK8TULDC5xuV1yDpiQrxP0su2tDNAOicMFQwsjr8+KAbyb1c1cBx4QsMENcXhDgXlqSVTBf2sDRQiaQ47CWHX1erc/1f9blTTBP5c8HMbTr1VLK42YlUE8T5gRvzdHFwVElDI5yYXGA5LtXwDbq9a7e0xisiZ7vpyn49+NlOKh4UHFzNhOgQJRmzz+Zq+3398PmMQZt7cng151Wi9L3MZClm4ZdUOewywIcwLAszY4q1gozMl8kI/HleaeOKdpxgI11lLQEEpxPs/528Jql4QBNcaQm2yO2vVRR2eEANYA6XYMjbYa9XhkCwzlEGGZVjCtjDrfX49+DqcSwsuWDt5PcgEBx7yNGABp5IiumrrT0BIXjxqEEedKoY15kGO2Ykp3wz6p2JNyWUigYzLfXi69Rw0uOvpjvZPgc+nCd29dG1mgl+rya071RzoK9Pse8Bq3ZpPNyblFuh7KKzq+z3fx++W1O+DhPPUf9IGlwOal6AeSOq1C8lid6Sms6m4Q3D6+nO4QKgGAHWv7Gya1WidpQB5a7aHaDOeG/FN1CZsF4QXmAbz9EHzAgrrV93ZMUgypO7KEVvGeXvDPghMiPOhwpGBlullTxNORYDwiTi+WiG9CgSNh6nWZlW8w8+W6mKQeaximfAL/VN+c7QUCQpONBPsPjYcZ8WuJmJn0YxrPJoS3n0mOuOfbe+Uq9mBfU5WZfvma+1KlYK+gNUdbLmv1HZiUwcwAO2HYW/EQOfU5YBjS97PO4MReqDzI7tY3pz2toxs5qHiy4hs0/PwOe2duWj/fDlsNFAKQnR6uuIAUBLt2S1ipAW6dxvYMzj5LhPXz4nDVnImsbSZw8zcViJlzylxrJZNBJYLeShAHm+VcIISoxED89/lL7kSWGlEHBrpMnpAdr77jmY0mkEHvOYsdw0G/S0tRTS8rACvP+tRRdmIj+QLQCRWY8w==");
213 return KeyPair.builder("kuali-devops").withPublicKey(publicKey).withPrivateKey(privateKey).build().getPrivateKey();
214 }
215
216 }