package com.sequenceiq.cloudbreak.orchestrator.salt.client; import static com.sequenceiq.cloudbreak.orchestrator.salt.client.SaltEndpoint.BOOT_HOSTNAME_ENDPOINT; import static java.util.Collections.singletonMap; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.ws.rs.client.Client; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.http.HttpStatus; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.glassfish.jersey.media.multipart.Boundary; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPart; import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.sequenceiq.cloudbreak.client.RestClientUtil; import com.sequenceiq.cloudbreak.client.PkiUtil; import com.sequenceiq.cloudbreak.orchestrator.exception.CloudbreakOrchestratorFailedException; import com.sequenceiq.cloudbreak.orchestrator.model.GatewayConfig; import com.sequenceiq.cloudbreak.orchestrator.model.GenericResponse; import com.sequenceiq.cloudbreak.orchestrator.model.GenericResponses; import com.sequenceiq.cloudbreak.orchestrator.salt.client.target.Target; import com.sequenceiq.cloudbreak.orchestrator.salt.domain.Pillar; import com.sequenceiq.cloudbreak.orchestrator.salt.domain.SaltAction; import com.sequenceiq.cloudbreak.util.JaxRSUtil; public class SaltConnector implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(SaltConnector.class); private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String SALT_USER = "saltuser"; private static final String SALT_PASSWORD = "saltpass"; private static final String SALT_BOOT_USER = "cbadmin"; private static final String SALT_BOOT_PASSWORD = "cbadmin"; private static final String SIGN_HEADER = "signature"; private static final List<Integer> ACCEPTED_STATUSES = Arrays.asList(HttpStatus.SC_OK, HttpStatus.SC_CREATED, HttpStatus.SC_ACCEPTED); private final Client restClient; private final WebTarget saltTarget; private final String saltPassword; private final String signatureKey; public SaltConnector(GatewayConfig gatewayConfig, boolean debug) { try { this.restClient = RestClientUtil.createClient( gatewayConfig.getServerCert(), gatewayConfig.getClientCert(), gatewayConfig.getClientKey(), debug, SaltConnector.class); String saltBootPasswd = Optional.ofNullable(gatewayConfig.getSaltBootPassword()).orElse(SALT_BOOT_PASSWORD); HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic(SALT_BOOT_USER, saltBootPasswd); this.saltTarget = restClient.target(gatewayConfig.getGatewayUrl()).register(feature); this.saltPassword = Optional.ofNullable(gatewayConfig.getSaltPassword()).orElse(SALT_PASSWORD); this.signatureKey = gatewayConfig.getSignatureKey(); } catch (Exception e) { throw new RuntimeException("Failed to create rest client with 2-way-ssl config", e); } } public GenericResponse health() { Response response = saltTarget.path(SaltEndpoint.BOOT_HEALTH.getContextPath()).request().get(); GenericResponse responseEntity = JaxRSUtil.response(response, GenericResponse.class); LOGGER.info("Health response: {}", responseEntity); return responseEntity; } public GenericResponses pillar(Set<String> targets, Pillar pillar) { Response distributeResponse = saltTarget.path(SaltEndpoint.BOOT_PILLAR_DISTRIBUTE.getContextPath()).request() .header(SIGN_HEADER, PkiUtil.generateSignature(signatureKey, toJson(pillar).getBytes())) .post(Entity.json(pillar)); if (distributeResponse.getStatus() == HttpStatus.SC_NOT_FOUND) { // simple pillar save for CB <= 1.14 distributeResponse.close(); Response singleResponse = saltTarget.path(SaltEndpoint.BOOT_PILLAR_SAVE.getContextPath()).request() .header(SIGN_HEADER, PkiUtil.generateSignature(signatureKey, toJson(pillar).getBytes())) .post(Entity.json(pillar)); GenericResponses genericResponses = new GenericResponses(); GenericResponse genericResponse = new GenericResponse(); genericResponse.setAddress(targets.iterator().next()); genericResponse.setStatusCode(singleResponse.getStatus()); genericResponses.setResponses(Collections.singletonList(genericResponse)); singleResponse.close(); return genericResponses; } return JaxRSUtil.response(distributeResponse, GenericResponses.class); } public GenericResponses action(SaltAction saltAction) { Response response = saltTarget.path(SaltEndpoint.BOOT_ACTION_DISTRIBUTE.getContextPath()).request() .header(SIGN_HEADER, PkiUtil.generateSignature(signatureKey, toJson(saltAction).getBytes())) .post(Entity.json(saltAction)); GenericResponses responseEntity = JaxRSUtil.response(response, GenericResponses.class); LOGGER.info("SaltAction response: {}", responseEntity); return responseEntity; } public <T> T run(String fun, SaltClientType clientType, Class<T> clazz, String... arg) { return run(null, fun, clientType, clazz, arg); } public <T> T run(Target<String> target, String fun, SaltClientType clientType, Class<T> clazz, String... arg) { Form form = new Form(); form = addAuth(form) .param("fun", fun) .param("client", clientType.getType()); if (target != null) { form = form.param("tgt", target.getTarget()) .param("expr_form", target.getType()); } if (arg != null) { if (clientType.equals(SaltClientType.LOCAL) || clientType.equals(SaltClientType.LOCAL_ASYNC)) { for (String a : arg) { form.param("arg", a); } } else { for (int i = 0; i < arg.length - 1; i = i + 2) { form.param(arg[i], arg[i + 1]); } } } Response response = saltTarget.path(SaltEndpoint.SALT_RUN.getContextPath()).request() .header(SIGN_HEADER, PkiUtil.generateSignature(signatureKey, toJson(form.asMap()).getBytes())) .post(Entity.form(form)); T responseEntity = JaxRSUtil.response(response, clazz); LOGGER.info("Salt run response: {}", responseEntity); return responseEntity; } public <T> T wheel(String fun, Collection<String> match, Class<T> clazz) { Form form = new Form(); form = addAuth(form) .param("fun", fun) .param("client", "wheel"); if (match != null && !match.isEmpty()) { form.param("match", match.stream().collect(Collectors.joining(","))); } Response response = saltTarget.path(SaltEndpoint.SALT_RUN.getContextPath()).request() .header(SIGN_HEADER, PkiUtil.generateSignature(signatureKey, toJson(form.asMap()).getBytes())) .post(Entity.form(form)); T responseEntity = JaxRSUtil.response(response, clazz); LOGGER.info("SaltAction response: {}", responseEntity); return responseEntity; } public GenericResponses upload(Set<String> targets, String path, String fileName, byte[] content) throws IOException { Response distributeResponse = upload(SaltEndpoint.BOOT_FILE_DISTRIBUTE.getContextPath(), targets, path, fileName, content); if (distributeResponse.getStatus() == HttpStatus.SC_NOT_FOUND) { // simple file upload for CB <= 1.14 distributeResponse.close(); Response singleResponse = upload(SaltEndpoint.BOOT_FILE_UPLOAD.getContextPath(), targets, path, fileName, content); GenericResponses genericResponses = new GenericResponses(); GenericResponse genericResponse = new GenericResponse(); genericResponse.setAddress(targets.iterator().next()); genericResponse.setStatusCode(singleResponse.getStatus()); genericResponses.setResponses(Collections.singletonList(genericResponse)); singleResponse.close(); return genericResponses; } return JaxRSUtil.response(distributeResponse, GenericResponses.class); } private Response upload(String endpoint, Set<String> targets, String path, String fileName, byte[] content) throws IOException { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content)) { StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", inputStream, fileName); MultiPart multiPart = new FormDataMultiPart().field("path", path).field("targets", String.join(",", targets)).bodyPart(streamDataBodyPart); MediaType contentType = MediaType.MULTIPART_FORM_DATA_TYPE; contentType = Boundary.addBoundary(contentType); String signature = PkiUtil.generateSignature(signatureKey, content); return saltTarget.path(endpoint).request().header(SIGN_HEADER, signature).post(Entity.entity(multiPart, contentType)); } } public Map<String, String> members(List<String> privateIps) throws CloudbreakOrchestratorFailedException { Map<String, List<String>> clients = singletonMap("clients", privateIps); Response response = saltTarget.path(BOOT_HOSTNAME_ENDPOINT.getContextPath()).request() .header(SIGN_HEADER, PkiUtil.generateSignature(signatureKey, toJson(clients).getBytes())) .post(Entity.json(clients)); GenericResponses responses = JaxRSUtil.response(response, GenericResponses.class); List<GenericResponse> failedResponses = responses.getResponses().stream() .filter(genericResponse -> !ACCEPTED_STATUSES.contains(genericResponse.getStatusCode())).collect(Collectors.toList()); if (!failedResponses.isEmpty()) { failedResponseErrorLog(failedResponses); String failedNodeAddresses = failedResponses.stream().map(GenericResponse::getAddress).collect(Collectors.joining(",")); throw new CloudbreakOrchestratorFailedException("Hostname resolution failed for nodes: " + failedNodeAddresses); } return responses.getResponses().stream().collect(Collectors.toMap(GenericResponse::getAddress, GenericResponse::getStatus)); } private void failedResponseErrorLog(List<GenericResponse> failedResponses) { StringBuilder failedResponsesErrorMessage = new StringBuilder(); failedResponsesErrorMessage.append("Failed response from salt bootstrap, endpoint: ").append(BOOT_HOSTNAME_ENDPOINT); for (GenericResponse failedResponse : failedResponses) { failedResponsesErrorMessage.append("\n").append("Status code: ").append(failedResponse.getStatusCode()); failedResponsesErrorMessage.append(" Error message: ").append(failedResponse.getStatus()); } LOGGER.error(failedResponsesErrorMessage.toString()); } private Form addAuth(Form form) { form.param("username", SALT_USER) .param("password", saltPassword) .param("eauth", "pam"); return form; } @Override public void close() throws IOException { if (restClient != null) { restClient.close(); } } public String getSaltPassword() { return saltPassword; } private String toJson(Object target) { try { return MAPPER.writeValueAsString(target); } catch (JsonProcessingException e) { throw new IllegalArgumentException(e); } } }