Skip to content
Snippets Groups Projects
Commit 35852e2f authored by Alexander Lovett's avatar Alexander Lovett
Browse files

Merge branch 'develop' into 'master'

V2.0.14.RELEASE

See merge request alexander.lovett/looking-glass-service!13
parents 3c174fcd a5f5da99
No related branches found
No related tags found
No related merge requests found
Showing
with 624 additions and 284 deletions
* 2.0.13
# 2.0.14
* [LGR-75](https://jira.software.geant.org/browse/LGR-73) - Externalise saml groups to role
* [LGR-73](https://jira.software.geant.org/browse/LGR-73) - Remove router endpoint
* [LGR-72](https://jira.software.geant.org/browse/LGR-72) - Check user is authenticated prior to executing commands on internal routers
# 2.0.13
* [LGR-70](https://jira.software.geant.org/browse/LGR-70) - Pull UI out of jar
# 2.0.12
......
# tells Lombok that this is the root directory and that it shouldn’t search parent directories for more configuration files
config.stopBubbling = true
# tells Lombok to add @lombok.Generated annotation to all generated methods
lombok.addLombokGeneratedAnnotation = true
\ No newline at end of file
......@@ -3,10 +3,10 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.geant</groupId>
<artifactId>looking-glass-service</artifactId>
<version>2.0.13.RELEASE</version>
<packaging>jar</packaging>
<groupId>org.geant</groupId>
<artifactId>looking-glass-service</artifactId>
<version>2.0.14.RELEASE</version>
<packaging>jar</packaging>
<name>looking-glass-service</name>
<description>Looking Glass Demo</description>
......@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
......@@ -25,25 +25,6 @@
<java.version>1.8</java.version>
</properties>
<scm>
<developerConnection>scm:git:https://gitlab.geant.net/live-projects/looking-glass-service.git
</developerConnection>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>looking-glass-releases</id>
<name>GEANT Artifactory-releases</name>
<url>https://artifactory.geant.net:443/artifactory/lg-release-local</url>
</repository>
<snapshotRepository>
<id>looking-glass-snapshots</id>
<name>GEANT Artifactory-snapshots</name>
<url>https://artifactory.geant.net:443/artifactory/lg-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -59,19 +40,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -87,6 +55,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
......@@ -101,16 +74,6 @@
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
......@@ -119,6 +82,50 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-statsd</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>com.tngtech.jgiven</groupId>
<artifactId>jgiven-junit</artifactId>
<version>0.18.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.jgiven</groupId>
<artifactId>jgiven-spring</artifactId>
<version>0.18.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.14.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alexlovett</groupId>
<artifactId>jgiven-extension</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.26.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
......@@ -138,7 +145,8 @@
<line>After=syslog.target</line>
<line>[Service]</line>
<line>User=${artifactId}</line>
<line>ExecStart=/opt/${artifactId}/latest/bin/${artifactId}.${project.packaging} --spring.config.location=file:/opt/${artifactId}/${version}/config/application.yml</line>
<line>ExecStart=/opt/${artifactId}/latest/bin/${artifactId}.${project.packaging} --spring.config.location=file:/opt/${artifactId}/${version}/config/application.yml
</line>
<line>StandardOutput=syslog</line>
<line>StandardError=syslog</line>
<line>SuccessExitStatus=143</line>
......@@ -188,13 +196,15 @@
<version>0.8.5</version>
<executions>
<execution>
<id>pre-integration-test</id>
<phase>test-compile</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
......@@ -277,8 +287,11 @@
<ruleset>default</ruleset>
</entry>
<entry>
<name>/opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging}</name>
<file>${project.build.directory}/${project.build.finalName}.${project.packaging}</file>
<name>
/opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging}
</name>
<file>${project.build.directory}/${project.build.finalName}.${project.packaging}
</file>
<ruleset>default</ruleset>
</entry>
<entry>
......@@ -288,7 +301,9 @@
</entry>
<entry>
<name>/opt/${artifactId}/${version}/bin/${artifactId}.${project.packaging}</name>
<linkTo>/opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging}</linkTo>
<linkTo>
/opt/${artifactId}/${version}/bin/${project.build.finalName}.${project.packaging}
</linkTo>
<ruleset>default</ruleset>
</entry>
<entry>
......@@ -301,6 +316,34 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.18.1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.tngtech.jgiven</groupId>
<artifactId>jgiven-maven-plugin</artifactId>
<version>0.18.2</version>
<executions>
<execution>
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
<configuration>
<format>html</format>
</configuration>
</plugin>
</plugins>
</build>
......
File deleted
package org.geant.lgservice;
import org.geant.lgservice.infrastructure.rest.inventoryprovider.InventoryProviderConfig;
import org.geant.lgservice.infrastructure.ssh.SSHConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@EnableSwagger2
@SpringBootApplication
@Import({
InventoryProviderConfig.class,
SSHConfig.class
})
public class LookingGlassServiceApplication {
public static void main(String[] args) {
SpringApplication.run(LookingGlassServiceApplication.class, args);
}
@Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.basePackage("org.geant.lgservice")).build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("Looking Glass Rest APIs")
.description("This page lists all the rest apis for Looking Glass Application.").build();
}
}
......@@ -30,18 +30,6 @@ public class AppConfig implements Serializable {
@Value("${configuration.publickey.keyfile}")
private String keyFile;
@Value("${spring.datasource.username}")
private String dbUsername;
@Value("${spring.datasource.password}")
private String dbPassword;
@Value("${spring.datasource.url}")
private String dbURL;
@Value("${spring.datasource.driverClassName}")
private String dbDriverClassName;
@Value("${saml.metadata-url}")
private String metadataURL;
......
package org.geant.lgservice.config;
import java.io.InputStream;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.geant.lgservice.exceptions.TechnicalException;
import org.geant.lgservice.pojos.Group;
import org.geant.lgservice.security.LGUser;
import org.geant.lgservice.utils.LGRole;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
public class CommandsParser {
private static final Logger logger = LogManager.getLogger(CommandsParser.class);
public List<Group> getCommandsFromXML(final String fileName, final LGUser user) {
Commands commands = new Commands();
try {
String newFileName = resolveFileName(fileName, user);
if (newFileName != null) {
ClassLoader classLoader = getClass().getClassLoader();
InputStream stream = classLoader.getResourceAsStream(newFileName);
JAXBContext jaxbContext = JAXBContext.newInstance(Commands.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
commands = (Commands) unmarshaller.unmarshal(stream);
} else {
logger.error("Unable to read config file for commands list");
}
} catch (JAXBException e) {
logger.error("Unable to parse xml for " + fileName, e);
}
return commands.getGroups();
}
public String resolveFileName(final String fileName, final LGUser user) {
String newFileName = fileName + "_" + LGRole.PUBLIC.getName();
if (user != null) {
switch (user.getRole()) {
case PUBLIC: {
newFileName = fileName + "_" + LGRole.PUBLIC.getName();
break;
}
case INTERNAL: {
newFileName = fileName + "_" + LGRole.INTERNAL.getName();
break;
}
case EXTERNAL: {
newFileName = fileName + "_" + LGRole.EXTERNAL.getName();
break;
}
}
}
newFileName = newFileName + ".xml";
return newFileName;
}
private static final Logger logger = LogManager.getLogger(CommandsParser.class);
public List<Group> getCommandsFromXML(final String fileName, final LGUser user) {
try {
InputStream stream = getClass().getClassLoader().getResourceAsStream(resolveFileName(fileName, user));
JAXBContext jaxbContext = JAXBContext.newInstance(Commands.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return ((Commands) unmarshaller.unmarshal(stream)).getGroups();
} catch (JAXBException e) {
logger.error("Unable to parse xml for " + fileName, e);
throw new TechnicalException(e);
}
}
@RequiredArgsConstructor
private enum GroupRanking {
INTERNAL(0),
EXTERNAL(1);
@Getter
private final int rank;
}
public String resolveFileName(final String fileName, final LGUser user) {
String accessLevel = Optional.ofNullable(user)
.map(UserDetails::getAuthorities)
.flatMap(authorities -> authorities
.stream()
.map(GrantedAuthority::getAuthority)
.map(GroupRanking::valueOf)
.reduce((groupOne, groupTwo) -> groupOne.getRank() < groupTwo.getRank() ? groupOne : groupTwo))
.map(GroupRanking::name)
.map(String::toLowerCase)
.orElse("public");
return String.format("%s_%s.xml", fileName, accessLevel);
}
}
package org.geant.lgservice.domain;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@Builder
@RequiredArgsConstructor
public class Router {
private final String hostName;
private final Access access;
public enum Access {
INTERNAL,
PUBLIC
}
}
package org.geant.lgservice.domain;
import java.util.Optional;
public interface RouterRepository {
Optional<Router> getByHostName(String hostname);
}
package org.geant.lgservice.ecmr;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.geant.lgservice.pojos.Command;
import org.geant.lgservice.pojos.CommandOutput;
import org.geant.lgservice.pojos.Credentials;
import org.geant.lgservice.pojos.Router;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
public class CallableCommandExecutor implements Callable<CommandOutput> {
private static final Logger logger = LogManager.getLogger(CallableCommandExecutor.class);
private Router router;
private Command command;
private String arguments;
private boolean displayAsXML;
private Credentials credentials;
CallableCommandExecutor(final Router router, final Command command, final String arguments,
final boolean displayAsXML, final Credentials credentials) {
this.router = router;
this.command = command;
this.arguments = arguments;
this.displayAsXML = displayAsXML;
this.credentials = credentials;
}
@Override
public CommandOutput call() {
long start = System.currentTimeMillis();
StringBuilder output = new StringBuilder();
CommandOutput finalOutput = new CommandOutput();
Connection conn = new Connection(router.getName());
try {
try {
conn.connect(null, 30000, 30000); // Should be configurable but this code is due a rewrite / pulled out
} catch (IOException e) {
logger.error("Error while connecting to router " + router.getName(), e);
return new CommandOutput(router.getName(), "Error while connecting to router " + router.getName(), null);
}
boolean isAuthenticated;
try {
isAuthenticated = conn.authenticateWithPublicKey(credentials.getUsername(),
new File(credentials.getKeyFile()), credentials.getPassword());
} catch (IOException e) {
logger.error("Error while authenticating the user for router " + router.getName(), e);
return new CommandOutput(router.getName(),
"Error while authenticating the user for router " + router.getName(), null);
}
if (isAuthenticated) {
Session session;
try {
session = conn.openSession();
} catch (IOException e) {
logger.error("Error while opening session to router " + router.getName(), e);
return new CommandOutput(router.getName(), "Error while opening session to router " + router.getName(),
null);
}
InputStream in = session.getStdout();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
try {
session.execCommand(getCommandWithArguments());
} catch (IOException e) {
logger.error("Error while executing command " + command.getValue(), e);
return new CommandOutput(router.getName(), "Error while executing command " + command.getValue(), null);
}
while (true) {
String line = "";
try {
line = br.readLine();
if (line == null)
break;
output.append(line + "\n");
} catch (IOException e) {
return new CommandOutput(router.getName(), "Error while reading line", null);
}
}
long finish = System.currentTimeMillis();
finalOutput.setRouterName(router.getName());
finalOutput.setCommandResult(output.toString());
finalOutput.setExecutionTime(finish - start);
return finalOutput;
}
return new CommandOutput(router.getName(), "Unable to authenticate user for router " + router.getName(), null);
} finally {
conn.close();
}
}
public String getCommandWithArguments() {
return Stream.of(
this.command.getValue(),
this.arguments,
this.displayAsXML ? "| display xml" : "")
.filter(StringUtils::isNotBlank)
.collect(Collectors.joining(" "));
}
}
package org.geant.lgservice.ecmr;
import org.geant.lgservice.config.AppConfig;
import org.geant.lgservice.pojos.Command;
import org.geant.lgservice.pojos.CommandOutput;
import org.geant.lgservice.pojos.Credentials;
import org.geant.lgservice.pojos.Router;
import org.springframework.stereotype.Component;
@Component
public class CommandExecutor {
private final Credentials credentials;
public CommandExecutor(AppConfig config) {
credentials = Credentials.builder()
.username(config.getUsername())
.password(config.getPassword())
.keyFile(config.getKeyFile())
.build();
}
public CommandOutput execute(Router router, Command command, String arguments, boolean asXml) {
return new CallableCommandExecutor(router, command, arguments, asXml, credentials).call();
}
}
package org.geant.lgservice.exceptions;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import static lombok.AccessLevel.PRIVATE;
@RequiredArgsConstructor(access = PRIVATE)
public class BusinessException extends RuntimeException {
@Getter
private final Reason reason;
@AllArgsConstructor
public enum Reason {
UNAUTHORIZED("User is not logged in"),
FORBIDDEN("User does not have access rights"),
NOT_FOUND("Could not find router");
@Getter
private final String description;
}
public static BusinessException withReason(Reason reason) {
return new BusinessException(reason);
}
@Override
public String getMessage() {
return reason.getDescription();
}
}
package org.geant.lgservice.exceptions;
public class TechnicalException extends RuntimeException {
public TechnicalException(Throwable t) {
super(t);
}
public TechnicalException(String message) {
super(message);
}
}
package org.geant.lgservice.infrastructure.rest.inventoryprovider;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.geant.lgservice.domain.RouterRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class InventoryProviderConfig {
@Bean
public RouterRepository routerRepository(@Value("${inventoryprovider.baseUrl}")String baseUrl) {
return InventoryProviderRouterRepository.builder().baseUrl(baseUrl)
.client(new OkHttpClient.Builder().addInterceptor(new OkHttpLogger()).build()).build();
}
@Slf4j
private static class OkHttpLogger implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
log.info("Making request to {} with headers {}", request.url().toString(), request.headers());
Response response = chain.proceed(request);
long contentLength = response.body().contentLength();
log.info("Response recieved with status {} and body {}", response.code(), response.peekBody(contentLength >= 0 ? contentLength : Long.MAX_VALUE).string());
return response;
}
}
}
package org.geant.lgservice.infrastructure.rest.inventoryprovider;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import org.geant.lgservice.domain.Router;
import org.geant.lgservice.domain.RouterRepository;
import org.geant.lgservice.exceptions.TechnicalException;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.http.GET;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.nio.charset.Charset.defaultCharset;
import static org.geant.lgservice.domain.Router.Access.INTERNAL;
import static org.geant.lgservice.domain.Router.Access.PUBLIC;
@Slf4j
public class InventoryProviderRouterRepository implements RouterRepository {
private final InventoryProvider inventoryProvider;
@Override
public Optional<Router> getByHostName(String hostname) {
try {
Response<Collection<InventoryProvider.InventoryProviderRouter>> response = inventoryProvider.getRouters()
.execute();
if (!response.isSuccessful()) {
HttpStatus status = HttpStatus.valueOf(response.code());
HttpStatusCodeException rootException = status.is4xxClientError() ?
new HttpClientErrorException(status, response.message(), response.errorBody()
.bytes(), defaultCharset()) :
new HttpServerErrorException(status, response.message(), response.errorBody()
.bytes(), defaultCharset());
log.error(response.message(), rootException);
throw new TechnicalException(rootException);
}
return response.body()
.stream()
.filter(router -> router.getEquipmentName().equals(hostname))
.map(InventoryProvider.InventoryProviderRouter::toDomain)
.findFirst();
} catch (IOException e) {
throw new TechnicalException(e);
}
}
@Builder
public InventoryProviderRouterRepository(String baseUrl, OkHttpClient client) {
OkHttpClient.Builder builder =
(client != null ? client.newBuilder() : new OkHttpClient.Builder());
List<Interceptor> existingInterceptors = builder.interceptors();
List<Interceptor> interceptors = Stream.concat(
Stream.of((Interceptor.Chain chain) -> chain.proceed(chain.request().newBuilder()
.addHeader("Accept", "application/json")
.build())),
existingInterceptors.stream()).collect(Collectors.toList());
existingInterceptors.removeIf(interceptors::contains);
existingInterceptors.addAll(interceptors);
this.inventoryProvider = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(builder.build())
.addConverterFactory(JacksonConverterFactory.create())
.build()
.create(InventoryProvider.class);
}
interface InventoryProvider {
@GET("routers/all")
Call<Collection<InventoryProviderRouter>> getRouters();
@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
class InventoryProviderRouter {
private final String equipmentName;
private final String type;
@JsonCreator
public InventoryProviderRouter(
@JsonProperty("equipment name") String equipmentName,
@JsonProperty("type") String type
) {
this.equipmentName = equipmentName;
this.type = type;
}
private static final Map<String, Router.Access> ACCESS_MAPPINGS =
ImmutableMap.<String, Router.Access>builder()
.put("INTERNAL", INTERNAL)
.build();
public Router toDomain() {
return Router.builder()
.hostName(equipmentName)
.access(ACCESS_MAPPINGS.getOrDefault(type, PUBLIC))
.build();
}
}
}
}
package org.geant.lgservice.infrastructure.ssh;
import ch.ethz.ssh2.Connection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.geant.lgservice.exceptions.TechnicalException;
import java.io.File;
import java.io.IOException;
import static java.lang.String.format;
@RequiredArgsConstructor
@Slf4j
public class ConnectionFactory {
private final String username;
private final String password;
private final File keyFile;
private final int timeout;
public Connection createConnection(String hostname) {
log.info("Creating connection for {}", hostname);
Connection connection = new Connection(hostname);
try {
connection.connect(null, timeout, timeout);
if (!connection.authenticateWithPublicKey(username, keyFile, password)){
throw new TechnicalException(format("Could not authenticate ssh connection with %s", hostname));
}
} catch (IOException e) {
throw new TechnicalException(e);
}
log.info("Connection for {} created", hostname);
return connection;
}
}
package org.geant.lgservice.infrastructure.ssh;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.DISCONNECTED;
import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.ERROR;
@Slf4j
@Component
@AllArgsConstructor
public class ConnectionManager implements SSHHost.ConnectionStateListener {
private final ConnectionFactory connectionFactory;
private final Map<String, SSHHost> hosts = new ConcurrentHashMap<>();
public SSHHost getHost(String hostname) {
log.info("retrieving host {}", hostname);
synchronized (this) {
return hosts
.computeIfAbsent(
hostname,
host -> {
log.info("Host [{}] connection does not exist", hostname);
return SSHHost.fromConnection(connectionFactory.createConnection(host))
.withConnectionStateListener(this);
});
}
}
@Override
public void stateChanged(SSHHost host, State state) {
synchronized (this) {
log.info("host [{}] state changed to [{}]", host, state);
if (state.equals(DISCONNECTED) || state.equals(ERROR)) {
hosts.remove(host);
}
log.info("currently active connections: [{}]", hosts.keySet());
}
}
}
package org.geant.lgservice.infrastructure.ssh;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.geant.lgservice.pojos.CommandOutput;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Slf4j
@Component
@RequiredArgsConstructor
public class SSHCommandExecutor {
private final ConnectionManager connectionManager;
public CommandOutput execute(String hostname, String command) {
log.info("Executing command [{}] on host [{}]", command, hostname);
StopWatch timer = new StopWatch();
timer.start();
String response = connectionManager.getHost(hostname)
.execute(command);
timer.stop();
log.info("Command completed on host [{}] in [{}]ms with result [{}]", hostname, timer.getTotalTimeMillis(), response);
return CommandOutput.builder()
.routerName(hostname)
.commandResult(response)
.executionTime(timer.getTotalTimeMillis())
.build();
}
}
package org.geant.lgservice.infrastructure.ssh;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.File;
@Configuration
public class SSHConfig {
@Bean
public ConnectionFactory connectionFactory(
@Value("${configuration.publickey.username}") String username,
@Value("${configuration.publickey.keyfile}") File keyFile,
@Value("${configuration.publickey.password}") String password,
@Value("${configuration.ssh.connection.timeout:30000}") int timeout
) {
return new ConnectionFactory(username, password, keyFile, timeout);
}
}
package org.geant.lgservice.infrastructure.ssh;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.ConnectionMonitor;
import ch.ethz.ssh2.Session;
import lombok.extern.slf4j.Slf4j;
import org.geant.lgservice.exceptions.TechnicalException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.DISCONNECTED;
import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.ERROR;
@Slf4j
public class SSHHost implements ConnectionMonitor {
private final Connection connection;
private final Collection<ConnectionStateListener> connectionStateListeners = new HashSet<>();
private SSHHost(Connection connection) {
this.connection = connection;
connection.addConnectionMonitor(this);
}
public static SSHHost fromConnection(Connection connection) {
if (!connection.isAuthenticationComplete()) {
throw new TechnicalException("Connection is not authenticated");
}
return new SSHHost(connection);
}
public String execute(String command) {
Session session = null;
try {
log.debug("opening session on connection [{}] with host [{}]", connection, hostname());
session = connection.openSession();
log.debug("Session [{}] opened", session);
log.debug("sending command [{}] to host [{}]", command, hostname());
session.execCommand(command);
log.debug("command [{}] sent to host [{}]", command, hostname());
int exitStatus = Optional.of(session).map(Session::getExitStatus).orElse(0);
log.debug("Exit status [{}] recieved", exitStatus);
InputStream response = exitStatus != 0 ? session.getStderr() : session.getStdout();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response))) {
return reader.lines().collect(Collectors.joining("\n"));
}
} catch (IOException e) {
triggerStateChangeListeners(ERROR);
connection.close();
throw new TechnicalException(e);
}
}
public String hostname() {
return connection.getHostname();
}
public SSHHost withConnectionStateListener(ConnectionStateListener listener) {
connectionStateListeners.add(listener);
return this;
}
private void triggerStateChangeListeners(ConnectionStateListener.State state) {
connectionStateListeners
.forEach(connectionStateListener -> connectionStateListener.stateChanged(this, state));
}
@Override
public void connectionLost(Throwable reason) {
log.warn("Host [{}] lost connection: ", connection.getHostname(), reason);
triggerStateChangeListeners(DISCONNECTED);
}
@FunctionalInterface
public interface ConnectionStateListener {
enum State {
DISCONNECTED,
ERROR
}
void stateChanged(SSHHost host, State state);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment