package org.ovirt.engine.core.vdsbroker.vdsbroker;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.ovirt.engine.core.common.businessentities.VmInit;
import org.ovirt.engine.core.common.businessentities.VmInitNetwork;
import org.ovirt.engine.core.utils.JsonHelper;
import org.ovirt.engine.core.utils.network.vm.VmInitNetworkIpInfoFetcher;
import org.ovirt.engine.core.utils.network.vm.VmInitNetworkIpv4InfoFetcher;
import org.ovirt.engine.core.utils.network.vm.VmInitNetworkIpv6InfoFetcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
public class CloudInitHandler {
private static final Logger log = LoggerFactory.getLogger(CloudInitHandler.class);
private final VmInit vmInit;
private final Map<String, Object> metaData;
private final Map<String, Object> userData;
private final Map<String, byte[]> files;
private int nextFileIndex;
private String interfaces;
private final String passwordKey = "password";
private enum CloudInitFileMode {
FILE,
NETWORK;
}
public CloudInitHandler(VmInit vmInit) {
this.vmInit = vmInit;
metaData = new HashMap<>();
userData = new HashMap<>();
files = new HashMap<>();
nextFileIndex = 0;
}
public Map<String, byte[]> getFileData()
throws UnsupportedEncodingException, IOException, JsonGenerationException, JsonMappingException {
if (vmInit != null) {
try {
storeHostname();
storeAuthorizedKeys();
storeRegenerateKeys();
storeNetwork();
storeTimeZone();
storeRootPassword();
storeUserName();
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Malformed input", ex);
}
}
// Add other required/supplemental data
storeExecutionParameters();
String metaDataStr = mapToJson(metaData);
String userDataStr = mapToYaml(userData);
if (vmInit != null && vmInit.getCustomScript() != null) {
userDataStr += vmInit.getCustomScript();
}
// add #cloud-config for user data file head
if (StringUtils.isNotBlank(userDataStr)) {
userDataStr = "#cloud-config\n" + userDataStr;
}
files.put("openstack/latest/meta_data.json", metaDataStr.getBytes("UTF-8"));
files.put("openstack/latest/user_data", userDataStr.getBytes("UTF-8"));
// mask password for log if exists
if (metaDataStr.contains(passwordKey) && vmInit != null && vmInit.getRootPassword() != null) {
String oldStr = String.format("\"%s\" : \"%s\"", passwordKey, vmInit.getRootPassword());
String newStr = String.format("\"%s\" : ***", passwordKey);
metaDataStr = metaDataStr.replace(oldStr, newStr);
}
log.debug("cloud-init meta-data:\n{}", metaDataStr);
log.debug("cloud-init user-data:\n{}", userDataStr);
return files;
}
private void storeHostname() {
if (!StringUtils.isEmpty(vmInit.getHostname())) {
metaData.put("hostname", vmInit.getHostname());
metaData.put("name", vmInit.getHostname());
}
}
private void storeAuthorizedKeys() {
if (!StringUtils.isEmpty(vmInit.getAuthorizedKeys())) {
metaData.put("public_keys", normalizeAuthorizedKeys(vmInit.getAuthorizedKeys()));
}
}
private List<String> normalizeAuthorizedKeys(String authorizedKeys) {
List<String> keys = new ArrayList<>();
for (String key : vmInit.getAuthorizedKeys().split("(\\r?\\n|\\r)+")) {
if (!StringUtils.isEmpty(key)) {
keys.add(key);
}
}
return keys;
}
private void storeRegenerateKeys() {
if (vmInit.getRegenerateKeys() != null && vmInit.getRegenerateKeys()) {
// Create new system ssh keys
userData.put("ssh_deletekeys", "True");
}
}
private void storeNetwork() throws UnsupportedEncodingException {
StringBuilder output = new StringBuilder();
if (vmInit.getNetworks() != null) {
List<VmInitNetwork> networks = vmInit.getNetworks();
for (VmInitNetwork iface: networks) {
if (Boolean.TRUE.equals(iface.getStartOnBoot())) {
output.append("auto ").append(iface.getName()).append("\n");
}
storeIpv4(iface, output);
// As of cloud-init 0.7.1, you can't set DNS servers without also setting NICs
if (vmInit.getDnsServers() != null) {
output.append(" dns-nameservers")
.append(" ").append(vmInit.getDnsServers());
output.append("\n");
}
if (vmInit.getDnsSearch() != null) {
output.append(" dns-search")
.append(" ").append(vmInit.getDnsSearch());
output.append("\n");
}
storeIpv6(iface, output);
}
}
interfaces = output.toString();
if (!interfaces.isEmpty()) {
// Workaround for cloud-init 0.6.3, which requires the "network-interfaces"
// meta-data entry instead of the "network_config" file reference
metaData.put("network-interfaces", interfaces);
// Cloud-init will translate this as needed for ifcfg-based systems
storeNextFile(CloudInitFileMode.NETWORK, "/etc/network/interfaces", interfaces.getBytes("US-ASCII"));
}
}
private void storeIpv4(VmInitNetwork iface, StringBuilder output) {
storeIp("inet", new VmInitNetworkIpv4InfoFetcher(iface), output);
}
private void storeIpv6(VmInitNetwork iface, StringBuilder output) {
storeIp("inet6", new VmInitNetworkIpv6InfoFetcher(iface), output);
}
private void storeIp(String ipStack, VmInitNetworkIpInfoFetcher ipInfoFetcher, StringBuilder output) {
output.append(String.format("iface %s %s %s%n",
ipInfoFetcher.fetchName(),
ipStack,
ipInfoFetcher.fetchBootProtocol()));
if (StringUtils.isNotEmpty(ipInfoFetcher.fetchIp())) {
output.append(String.format(" address %s%n", ipInfoFetcher.fetchIp()));
}
if (StringUtils.isNotEmpty(ipInfoFetcher.fetchNetmask())) {
output.append(String.format(" netmask %s%n", ipInfoFetcher.fetchNetmask()));
}
if (StringUtils.isNotEmpty(ipInfoFetcher.fetchGateway())) {
output.append(String.format(" gateway %s%n", ipInfoFetcher.fetchGateway()));
}
}
private void storeTimeZone() {
if (vmInit.getTimeZone() != null) {
userData.put("timezone", vmInit.getTimeZone());
}
}
private void storeRootPassword() {
if (!StringUtils.isEmpty(vmInit.getRootPassword())) {
// Note that this is in plain text in the config disk
userData.put(passwordKey, vmInit.getRootPassword());
}
}
private void storeUserName() {
if (!StringUtils.isEmpty(vmInit.getUserName())) {
userData.put("user", vmInit.getUserName());
}
}
private void storeExecutionParameters() {
// Store defaults in meta-data and user-data that apply regardless
// of parameters passed in from the user.
// New instance id required for cloud-init to process data on startup
metaData.put("uuid", UUID.randomUUID().toString());
Map<String, String> meta = new HashMap<>();
// Local allows us to set up networking
meta.put("dsmode", "local");
meta.put("essential", "false");
meta.put("role", "server");
metaData.put("meta", meta);
metaData.put("launch_index", "0");
metaData.put("availability_zone", "nova");
userData.put("disable_root", 0);
// Redirect log output from cloud-init execution from terminal
Map<String, String> output = new HashMap<>();
output.put("all", ">> /var/log/cloud-init-output.log");
userData.put("output", output);
// Disable metadata-server-based datasources to prevent long boot times
List<String> runcmd = new ArrayList<>();
runcmd.add("sed -i '/^datasource_list: /d' /etc/cloud/cloud.cfg; echo 'datasource_list: [\"NoCloud\", \"ConfigDrive\"]' >> /etc/cloud/cloud.cfg");
userData.put("runcmd", runcmd);
Map<String, Object> opts = new HashMap<>();
opts.put("expire", false);
userData.put("chpasswd", opts);
userData.put("ssh_pwauth", true);
}
private void storeNextFile(CloudInitFileMode fileMode, String destinationPath, byte[] data) {
String contentPath = String.format("/content/%04d", nextFileIndex++);
Map<String, String> mdEntry = new HashMap<>();
mdEntry.put("content_path", contentPath);
mdEntry.put("path", destinationPath);
if (fileMode == CloudInitFileMode.FILE) {
if (!metaData.containsKey("files")) {
metaData.put("files", new ArrayList<Map<String, String>>());
}
@SuppressWarnings("unchecked")
List<Map<String, String>> mdFiles = (ArrayList<Map<String, String>>) metaData.get("files");
mdFiles.add(mdEntry);
}
else {
metaData.put("network_config", mdEntry);
}
files.put("openstack" + contentPath, data);
}
private String mapToJson(Map<String, Object> input) throws IOException {
return JsonHelper.mapToJson(input);
}
private String mapToYaml(Map<String, Object> input) {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
return yaml.dump(input);
}
}