Added integration option with device simulator.
Signed-off-by: Kai Zimmermann <kai.zimmermann@bosch-si.com>
This commit is contained in:
@@ -9,8 +9,12 @@
|
||||
package org.eclipse.hawkbit.mgmt.client;
|
||||
|
||||
import org.eclipse.hawkbit.feign.core.client.FeignClientConfiguration;
|
||||
import org.eclipse.hawkbit.feign.core.client.IgnoreMultipleConsumersProducersSpringMvcContract;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.MgmtSoftwareModuleClientResource;
|
||||
import org.eclipse.hawkbit.mgmt.client.scenarios.ConfigurableScenario;
|
||||
import org.eclipse.hawkbit.mgmt.client.scenarios.CreateStartedRolloutExample;
|
||||
import org.eclipse.hawkbit.mgmt.client.scenarios.upload.FeignMultipartEncoder;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
@@ -18,11 +22,19 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.netflix.feign.EnableFeignClients;
|
||||
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.hateoas.hal.Jackson2HalModule;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
import feign.auth.BasicAuthRequestInterceptor;
|
||||
import feign.jackson.JacksonDecoder;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableFeignClients
|
||||
@@ -72,6 +84,21 @@ public class Application implements CommandLineRunner {
|
||||
return new CreateStartedRolloutExample();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MgmtSoftwareModuleClientResource uploadSoftwareModule() {
|
||||
final ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.registerModule(new Jackson2HalModule());
|
||||
|
||||
return Feign.builder().contract(new IgnoreMultipleConsumersProducersSpringMvcContract())
|
||||
.requestInterceptor(
|
||||
new BasicAuthRequestInterceptor(configuration.getUsername(), configuration.getPassword()))
|
||||
.logger(new Logger.ErrorLogger()).encoder(new FeignMultipartEncoder())
|
||||
.decoder(new ResponseEntityDecoder(new JacksonDecoder(mapper)))
|
||||
.target(MgmtSoftwareModuleClientResource.class,
|
||||
configuration.getUrl() + MgmtRestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING);
|
||||
}
|
||||
|
||||
private boolean containsArg(final String containsArg, final String... args) {
|
||||
for (final String arg : args) {
|
||||
if (arg.equalsIgnoreCase(containsArg)) {
|
||||
|
||||
@@ -40,6 +40,7 @@ public class ClientConfigurationProperties {
|
||||
private final List<Scenario> scenarios = new ArrayList<>();
|
||||
|
||||
public static class Scenario {
|
||||
private String tenant = "DEFAULT";
|
||||
private int targets = 100;
|
||||
private int distributionSets = 10;
|
||||
private int appModulesPerDistributionSet = 2;
|
||||
@@ -47,6 +48,46 @@ public class ClientConfigurationProperties {
|
||||
private String smSwName = "Application";
|
||||
private String smFwName = "Firmware";
|
||||
private String targetName = "Device";
|
||||
private int artifactsPerSM = 1;
|
||||
private String targetAddress = "amqp:/simulator.replyTo";
|
||||
|
||||
/**
|
||||
* Artifact size. Values can use the suffixed "MB" or "KB" to indicate a
|
||||
* Megabyte or Kilobyte size.
|
||||
*/
|
||||
private String artifactSize = "1MB";
|
||||
|
||||
public String getTargetAddress() {
|
||||
return targetAddress;
|
||||
}
|
||||
|
||||
public void setTargetAddress(final String targetAddress) {
|
||||
this.targetAddress = targetAddress;
|
||||
}
|
||||
|
||||
public String getTenant() {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
public void setTenant(final String tenant) {
|
||||
this.tenant = tenant;
|
||||
}
|
||||
|
||||
public int getArtifactsPerSM() {
|
||||
return artifactsPerSM;
|
||||
}
|
||||
|
||||
public void setArtifactsPerSM(final int artifactsPerSM) {
|
||||
this.artifactsPerSM = artifactsPerSM;
|
||||
}
|
||||
|
||||
public String getArtifactSize() {
|
||||
return artifactSize;
|
||||
}
|
||||
|
||||
public void setArtifactSize(final String artifactSize) {
|
||||
this.artifactSize = artifactSize;
|
||||
}
|
||||
|
||||
public String getTargetName() {
|
||||
return targetName;
|
||||
|
||||
@@ -9,20 +9,25 @@
|
||||
package org.eclipse.hawkbit.mgmt.client.scenarios;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.eclipse.hawkbit.mgmt.client.ClientConfigurationProperties;
|
||||
import org.eclipse.hawkbit.mgmt.client.ClientConfigurationProperties.Scenario;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.MgmtDistributionSetClientResource;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.MgmtSoftwareModuleClientResource;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.MgmtSystemManagementClientResource;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.MgmtTargetClientResource;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.builder.DistributionSetBuilder;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.builder.SoftwareModuleAssigmentBuilder;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.builder.SoftwareModuleBuilder;
|
||||
import org.eclipse.hawkbit.mgmt.client.resource.builder.TargetBuilder;
|
||||
import org.eclipse.hawkbit.mgmt.client.scenarios.upload.ArtifactFile;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -37,11 +42,19 @@ public class ConfigurableScenario {
|
||||
private MgmtDistributionSetClientResource distributionSetResource;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("mgmtSoftwareModuleClientResource")
|
||||
private MgmtSoftwareModuleClientResource softwareModuleResource;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("uploadSoftwareModule")
|
||||
private MgmtSoftwareModuleClientResource uploadSoftwareModule;
|
||||
|
||||
@Autowired
|
||||
private MgmtTargetClientResource targetResource;
|
||||
|
||||
@Autowired
|
||||
private MgmtSystemManagementClientResource systemManagementResource;
|
||||
|
||||
@Autowired
|
||||
private ClientConfigurationProperties clientConfigurationProperties;
|
||||
|
||||
@@ -56,37 +69,76 @@ public class ConfigurableScenario {
|
||||
}
|
||||
|
||||
private void createScenario(final Scenario scenario) {
|
||||
systemManagementResource.deleteTenant(scenario.getTenant());
|
||||
createTargets(scenario);
|
||||
createDistributionSets(scenario);
|
||||
}
|
||||
|
||||
private void createDistributionSets(final Scenario scenario) {
|
||||
final byte[] artifact = generateArtifact(scenario);
|
||||
|
||||
distributionSetResource
|
||||
.createDistributionSets(new DistributionSetBuilder().name(scenario.getDsName()).type("os_app")
|
||||
.version("1.0.").buildAsList(scenario.getDistributionSets()))
|
||||
.getBody().parallelStream().forEach(dsSet -> {
|
||||
final List<MgmtSoftwareModule> modules = softwareModuleResource
|
||||
.createSoftwareModules(new SoftwareModuleBuilder().name(scenario.getSmFwName())
|
||||
.version(dsSet.getVersion()).type("os").build())
|
||||
.getBody();
|
||||
modules.addAll(
|
||||
softwareModuleResource
|
||||
.createSoftwareModules(new SoftwareModuleBuilder().name(scenario.getSmSwName())
|
||||
.version(dsSet.getVersion() + ".").type("application")
|
||||
.buildAsList(scenario.getAppModulesPerDistributionSet()))
|
||||
.getBody());
|
||||
final List<MgmtSoftwareModule> modules = addModules(scenario, dsSet, artifact);
|
||||
|
||||
final SoftwareModuleAssigmentBuilder assign = new SoftwareModuleAssigmentBuilder();
|
||||
modules.forEach(module -> assign.id(module.getModuleId()));
|
||||
|
||||
distributionSetResource.assignSoftwareModules(dsSet.getDsId(), assign.build());
|
||||
});
|
||||
}
|
||||
|
||||
private List<MgmtSoftwareModule> addModules(final Scenario scenario, final MgmtDistributionSet dsSet,
|
||||
final byte[] artifact) {
|
||||
final List<MgmtSoftwareModule> modules = softwareModuleResource.createSoftwareModules(
|
||||
new SoftwareModuleBuilder().name(scenario.getSmFwName()).version(dsSet.getVersion()).type("os").build())
|
||||
.getBody();
|
||||
modules.addAll(softwareModuleResource
|
||||
.createSoftwareModules(
|
||||
new SoftwareModuleBuilder().name(scenario.getSmSwName()).version(dsSet.getVersion() + ".")
|
||||
.type("application").buildAsList(scenario.getAppModulesPerDistributionSet()))
|
||||
.getBody());
|
||||
|
||||
for (int x = 0; x < scenario.getArtifactsPerSM(); x++) {
|
||||
modules.forEach(module -> {
|
||||
final ArtifactFile file = new ArtifactFile("dummyfile.dummy", null, "multipart/form-data", artifact);
|
||||
uploadSoftwareModule.uploadArtifact(module.getModuleId(), file, null, null, null);
|
||||
});
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
private byte[] generateArtifact(final Scenario scenario) {
|
||||
// create random object
|
||||
final Random random = new Random();
|
||||
|
||||
// create byte array
|
||||
final byte[] nbyte = new byte[parseSize(scenario.getArtifactSize())];
|
||||
|
||||
// put the next byte in the array
|
||||
random.nextBytes(nbyte);
|
||||
|
||||
return nbyte;
|
||||
}
|
||||
|
||||
private void createTargets(final Scenario scenario) {
|
||||
for (int i = 0; i < scenario.getTargets() / 100; i++) {
|
||||
targetResource.createTargets(new TargetBuilder().controllerId(scenario.getTargetName()).buildAsList(i * 100,
|
||||
(i + 1) * 100 > scenario.getTargets() ? scenario.getTargets() : (i + 1) * 100));
|
||||
targetResource.createTargets(new TargetBuilder().controllerId(scenario.getTargetName())
|
||||
.address(scenario.getTargetAddress()).buildAsList(i * 100,
|
||||
(i + 1) * 100 > scenario.getTargets() ? scenario.getTargets() - i * 100 : 100));
|
||||
}
|
||||
}
|
||||
|
||||
private int parseSize(final String s) {
|
||||
final String size = s.toUpperCase();
|
||||
if (size.endsWith("KB")) {
|
||||
return Integer.valueOf(size.substring(0, size.length() - 2)) * 1024;
|
||||
}
|
||||
if (size.endsWith("MB")) {
|
||||
return Integer.valueOf(size.substring(0, size.length() - 2)) * 1024 * 1024;
|
||||
}
|
||||
return Integer.valueOf(size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
/**
|
||||
* Example for creating and starting a Rollout.
|
||||
@@ -45,6 +46,7 @@ public class CreateStartedRolloutExample {
|
||||
private MgmtDistributionSetClientResource distributionSetResource;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("mgmtSoftwareModuleClientResource")
|
||||
private MgmtSoftwareModuleClientResource softwareModuleResource;
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.mgmt.client.scenarios.upload;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public class ArtifactFile implements MultipartFile {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String originalFilename;
|
||||
|
||||
private final String contentType;
|
||||
|
||||
private final byte[] content;
|
||||
|
||||
/**
|
||||
* Create a new ArtifactFile with the given content.
|
||||
*
|
||||
* @param name
|
||||
* the name of the file
|
||||
* @param content
|
||||
* the content of the file
|
||||
*/
|
||||
public ArtifactFile(final String name, final byte[] content) {
|
||||
this(name, "", null, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ArtifactFile with the given content.
|
||||
*
|
||||
* @param name
|
||||
* of the file
|
||||
* @param originalFilename
|
||||
* the original filename (as on the client's machine)
|
||||
* @param contentType
|
||||
* the content type
|
||||
* @param content
|
||||
* of the file
|
||||
*/
|
||||
public ArtifactFile(final String name, final String originalFilename, final String contentType,
|
||||
final byte[] content) {
|
||||
Assert.hasLength(name, "Name must not be null");
|
||||
this.name = name;
|
||||
this.originalFilename = originalFilename != null ? originalFilename : "";
|
||||
this.contentType = contentType;
|
||||
this.content = content != null ? content : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return this.originalFilename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return this.content.length == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return this.content.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() throws IOException {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(this.content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(final File dest) throws IOException {
|
||||
FileCopyUtils.copy(this.content, dest);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2011-2015 Bosch Software Innovations GmbH, Germany. All rights reserved.
|
||||
*/
|
||||
package org.eclipse.hawkbit.mgmt.client.scenarios.upload;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import feign.RequestTemplate;
|
||||
import feign.codec.EncodeException;
|
||||
import feign.codec.Encoder;
|
||||
|
||||
/**
|
||||
* A feign encoder implementation which handles {@link MultipartFile} body.
|
||||
*/
|
||||
public class FeignMultipartEncoder implements Encoder {
|
||||
|
||||
private final List<HttpMessageConverter<?>> converters = new RestTemplate().getMessageConverters();
|
||||
private final HttpHeaders multipartHeaders = new HttpHeaders();
|
||||
private final HttpHeaders jsonHeaders = new HttpHeaders();
|
||||
|
||||
public static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
public FeignMultipartEncoder() {
|
||||
multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
jsonHeaders.setContentType(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final Object object, final Type bodyType, final RequestTemplate template)
|
||||
throws EncodeException {
|
||||
|
||||
encodeMultipartFormRequest(object, template);
|
||||
|
||||
}
|
||||
|
||||
private void encodeMultipartFormRequest(final Object value, final RequestTemplate template) {
|
||||
if (value == null) {
|
||||
throw new EncodeException("Cannot encode request with null value.");
|
||||
}
|
||||
if (!isMultipartFile(value)) {
|
||||
throw new EncodeException("Only multipart can be handled by this encoder");
|
||||
}
|
||||
encodeRequest(encodeMultipartFile((MultipartFile) value), multipartHeaders, template);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void encodeRequest(final Object value, final HttpHeaders requestHeaders, final RequestTemplate template)
|
||||
throws EncodeException {
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
final HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders);
|
||||
try {
|
||||
final Class<?> requestType = value.getClass();
|
||||
final MediaType requestContentType = requestHeaders.getContentType();
|
||||
for (final HttpMessageConverter<?> messageConverter : converters) {
|
||||
if (messageConverter.canWrite(requestType, requestContentType)) {
|
||||
((HttpMessageConverter<Object>) messageConverter).write(value, requestContentType, dummyRequest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (final IOException ex) {
|
||||
throw new EncodeException("Cannot encode request.", ex);
|
||||
}
|
||||
final HttpHeaders headers = dummyRequest.getHeaders();
|
||||
if (headers != null) {
|
||||
for (final Entry<String, List<String>> entry : headers.entrySet()) {
|
||||
template.header(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
/*
|
||||
* we should use a template output stream... this will cause issues if
|
||||
* files are too big, since the whole request will be in memory.
|
||||
*/
|
||||
template.body(outputStream.toByteArray(), UTF_8);
|
||||
}
|
||||
|
||||
private MultiValueMap<String, Object> encodeMultipartFile(final MultipartFile file) {
|
||||
try {
|
||||
final MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>();
|
||||
multiValueMap.add("file", new MultipartFileResource(file.getName(), file.getSize(), file.getInputStream()));
|
||||
return multiValueMap;
|
||||
} catch (final IOException ex) {
|
||||
throw new EncodeException("Cannot encode request.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMultipartFile(final Object object) {
|
||||
return object instanceof MultipartFile;
|
||||
}
|
||||
|
||||
private class HttpOutputMessageImpl implements HttpOutputMessage {
|
||||
|
||||
private final OutputStream body;
|
||||
private final HttpHeaders headers;
|
||||
|
||||
public HttpOutputMessageImpl(final OutputStream body, final HttpHeaders headers) {
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getBody() throws IOException {
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy resource class. Wraps file content and its original name.
|
||||
*/
|
||||
static class MultipartFileResource extends InputStreamResource {
|
||||
|
||||
private final String filename;
|
||||
private final long size;
|
||||
|
||||
public MultipartFileResource(final String filename, final long size, final InputStream inputStream) {
|
||||
super(inputStream);
|
||||
this.size = size;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException, IllegalStateException {
|
||||
return super.getInputStream(); // To change body of generated
|
||||
// methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return size;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
# http://www.eclipse.org/legal/epl-v10.html
|
||||
#
|
||||
|
||||
hawkbit.url=localhost:8080
|
||||
hawkbit.url=http://localhost:8080
|
||||
hawkbit.username=admin
|
||||
hawkbit.password=admin
|
||||
|
||||
@@ -20,4 +20,5 @@ spring.main.show-banner=false
|
||||
#hawkbit.scenarios.[0].sm-sw-name=gettingstarted-example
|
||||
|
||||
hawkbit.scenarios.[0].targets=10000
|
||||
hawkbit.scenarios.[0].distribution-sets=100
|
||||
hawkbit.scenarios.[0].distribution-sets=100
|
||||
hawkbit.scenarios.[0].artifactsPerSM=0
|
||||
Reference in New Issue
Block a user