package org.oransc.policyagent.configuration;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.google.gson.JsonSyntaxException;
-import com.google.gson.TypeAdapterFactory;
-
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.time.Duration;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import java.util.Properties;
-import java.util.ServiceLoader;
import java.util.Vector;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
-import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsClient;
-import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsClientFactory;
-import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsRequests;
-import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.model.CbsRequest;
-import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.model.EnvProperties;
-import org.onap.dcaegen2.services.sdk.rest.services.model.logging.RequestDiagnosticContext;
import org.oransc.policyagent.exceptions.ServiceException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import reactor.core.Disposable;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
@EnableConfigurationProperties
@ConfigurationProperties("app")
public class ApplicationConfig {
- private static final Logger logger = LoggerFactory.getLogger(ApplicationConfig.class);
-
- @Value("#{systemEnvironment}")
- Properties systemEnvironment;
+ @NotEmpty
+ private String filepath;
- private Disposable refreshConfigTask = null;
private Collection<Observer> observers = new Vector<>();
-
private Map<String, RicConfig> ricConfigs = new HashMap<>();
- @NotEmpty
- private String filepath;
-
@Autowired
public ApplicationConfig() {
}
- protected String getLocalConfigurationFilePath() {
+ public String getLocalConfigurationFilePath() {
return this.filepath;
}
throw new ServiceException("Could not find ric: " + ricName);
}
- public void initialize() {
- stop();
- loadConfigurationFromFile();
-
- refreshConfigTask = createRefreshTask() //
- .subscribe(notUsed -> logger.info("Refreshed configuration data"),
- throwable -> logger.error("Configuration refresh terminated due to exception", throwable),
- () -> logger.error("Configuration refresh terminated"));
- }
-
public static enum RicConfigUpdate {
ADDED, CHANGED, REMOVED
}
this.observers.add(o);
}
- Mono<EnvProperties> getEnvironment(Properties systemEnvironment) {
- return EnvironmentProcessor.readEnvironmentVariables(systemEnvironment);
- }
-
- Flux<ApplicationConfig> createRefreshTask() {
- return getEnvironment(systemEnvironment) //
- .flatMap(this::createCbsClient) //
- .flatMapMany(this::periodicConfigurationUpdates) //
- .map(this::parseRicConfigurationfromConsul) //
- .onErrorResume(this::onErrorResume);
- }
-
- Mono<CbsClient> createCbsClient(EnvProperties env) {
- return CbsClientFactory.createCbsClient(env);
- }
-
- private Flux<JsonObject> periodicConfigurationUpdates(CbsClient cbsClient) {
- final Duration initialDelay = Duration.ZERO;
- final Duration refreshPeriod = Duration.ofMinutes(1);
- final CbsRequest getConfigRequest = CbsRequests.getAll(RequestDiagnosticContext.create());
- return cbsClient.updates(getConfigRequest, initialDelay, refreshPeriod);
- }
-
- private <R> Mono<R> onErrorResume(Throwable trowable) {
- logger.error("Could not refresh application configuration {}", trowable.toString());
- return Mono.empty();
- }
-
- private ApplicationConfig parseRicConfigurationfromConsul(JsonObject jsonObject) {
- try {
- ApplicationConfigParser parser = new ApplicationConfigParser();
- parser.parse(jsonObject);
- setConfiguration(parser.getRicConfigs());
-
- } catch (ServiceException e) {
- logger.error("Could not parse configuration {}", e.toString(), e);
- }
- return this;
- }
-
private class Notification {
final RicConfig ric;
final RicConfigUpdate event;
}
}
- private void setConfiguration(@NotNull Collection<RicConfig> ricConfigs) {
+ public void setConfiguration(@NotNull Collection<RicConfig> ricConfigs) {
Collection<Notification> notifications = new Vector<>();
synchronized (this) {
Map<String, RicConfig> newRicConfigs = new HashMap<>();
}
}
}
-
- public void stop() {
- if (refreshConfigTask != null) {
- refreshConfigTask.dispose();
- refreshConfigTask = null;
- }
- }
-
- /**
- * Reads the configuration from file.
- */
- public void loadConfigurationFromFile() {
- String filepath = getLocalConfigurationFilePath();
- if (filepath == null) {
- logger.debug("No localconfiguration file used");
- return;
- }
- GsonBuilder gsonBuilder = new GsonBuilder();
- ServiceLoader.load(TypeAdapterFactory.class).forEach(gsonBuilder::registerTypeAdapterFactory);
-
- try (InputStream inputStream = createInputStream(filepath)) {
- JsonParser parser = new JsonParser();
- JsonObject rootObject = getJsonElement(parser, inputStream).getAsJsonObject();
- if (rootObject == null) {
- throw new JsonSyntaxException("Root is not a json object");
- }
- ApplicationConfigParser appParser = new ApplicationConfigParser();
- appParser.parse(rootObject);
- setConfiguration(appParser.getRicConfigs());
- logger.info("Local configuration file loaded: {}", filepath);
- } catch (JsonSyntaxException | ServiceException | IOException e) {
- logger.trace("Local configuration file not loaded: {}", filepath, e);
- }
- }
-
- JsonElement getJsonElement(JsonParser parser, InputStream inputStream) {
- return parser.parse(new InputStreamReader(inputStream));
- }
-
- InputStream createInputStream(@NotNull String filepath) throws IOException {
- return new BufferedInputStream(new FileInputStream(filepath));
- }
}
import org.oransc.policyagent.exceptions.ServiceException;
-class ApplicationConfigParser {
+public class ApplicationConfigParser {
private static final String CONFIG = "config";
private static Gson gson = new GsonBuilder() //
* ========================LICENSE_END===================================
*/
-package org.oransc.policyagent.configuration;
+package org.oransc.policyagent.tasks;
import java.util.Optional;
import java.util.Properties;
--- /dev/null
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.policyagent.tasks;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapterFactory;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.time.Duration;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+import javax.validation.constraints.NotNull;
+
+import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsClient;
+import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsClientFactory;
+import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsRequests;
+import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.model.CbsRequest;
+import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.model.EnvProperties;
+import org.onap.dcaegen2.services.sdk.rest.services.model.logging.RequestDiagnosticContext;
+import org.oransc.policyagent.configuration.ApplicationConfig;
+import org.oransc.policyagent.configuration.ApplicationConfigParser;
+import org.oransc.policyagent.exceptions.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * Regularly refreshes the configuration from Consul.
+ */
+@Component
+public class RefreshConfigTask {
+
+ private static final Logger logger = LoggerFactory.getLogger(RefreshConfigTask.class);
+
+ @Value("#{systemEnvironment}")
+ public Properties systemEnvironment;
+
+ private final ApplicationConfig appConfig;
+ private Disposable refreshTask = null;
+
+ @Autowired
+ public RefreshConfigTask(ApplicationConfig appConfig) {
+ this.appConfig = appConfig;
+ }
+
+ public void start() {
+ logger.debug("Starting refreshConfigTask");
+ stop();
+ loadConfigurationFromFile();
+ refreshTask = createRefreshTask() //
+ .subscribe(notUsed -> logger.info("Refreshed configuration data"),
+ throwable -> logger.error("Configuration refresh terminated due to exception", throwable),
+ () -> logger.error("Configuration refresh terminated"));
+ }
+
+ public void stop() {
+ if (refreshTask != null) {
+ refreshTask.dispose();
+ refreshTask = null;
+ }
+ }
+
+ Flux<ApplicationConfig> createRefreshTask() {
+ return getEnvironment(systemEnvironment) //
+ .flatMap(this::createCbsClient) //
+ .flatMapMany(this::periodicConfigurationUpdates) //
+ .map(this::parseRicConfigurationfromConsul) //
+ .onErrorResume(this::onErrorResume);
+ }
+
+ Mono<EnvProperties> getEnvironment(Properties systemEnvironment) {
+ return EnvironmentProcessor.readEnvironmentVariables(systemEnvironment);
+ }
+
+ Mono<CbsClient> createCbsClient(EnvProperties env) {
+ return CbsClientFactory.createCbsClient(env);
+ }
+
+ private Flux<JsonObject> periodicConfigurationUpdates(CbsClient cbsClient) {
+ final Duration initialDelay = Duration.ZERO;
+ final Duration refreshPeriod = Duration.ofMinutes(1);
+ final CbsRequest getConfigRequest = CbsRequests.getAll(RequestDiagnosticContext.create());
+ return cbsClient.updates(getConfigRequest, initialDelay, refreshPeriod);
+ }
+
+ private <R> Mono<R> onErrorResume(Throwable trowable) {
+ logger.error("Could not refresh application configuration {}", trowable.toString());
+ return Mono.empty();
+ }
+
+ private ApplicationConfig parseRicConfigurationfromConsul(JsonObject jsonObject) {
+ try {
+ ApplicationConfigParser parser = new ApplicationConfigParser();
+ parser.parse(jsonObject);
+ this.appConfig.setConfiguration(parser.getRicConfigs());
+ } catch (ServiceException e) {
+ logger.error("Could not parse configuration {}", e.toString(), e);
+ }
+ return this.appConfig;
+ }
+
+ /**
+ * Reads the configuration from file.
+ */
+ public void loadConfigurationFromFile() {
+ String filepath = appConfig.getLocalConfigurationFilePath();
+ if (filepath == null) {
+ logger.debug("No localconfiguration file used");
+ return;
+ }
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ ServiceLoader.load(TypeAdapterFactory.class).forEach(gsonBuilder::registerTypeAdapterFactory);
+
+ try (InputStream inputStream = createInputStream(filepath)) {
+ JsonParser parser = new JsonParser();
+ JsonObject rootObject = getJsonElement(parser, inputStream).getAsJsonObject();
+ if (rootObject == null) {
+ throw new JsonSyntaxException("Root is not a json object");
+ }
+ ApplicationConfigParser appParser = new ApplicationConfigParser();
+ appParser.parse(rootObject);
+ appConfig.setConfiguration(appParser.getRicConfigs());
+ logger.info("Local configuration file loaded: {}", filepath);
+ } catch (JsonSyntaxException | ServiceException | IOException e) {
+ logger.trace("Local configuration file not loaded: {}", filepath, e);
+ }
+ }
+
+ JsonElement getJsonElement(JsonParser parser, InputStream inputStream) {
+ return parser.parse(new InputStreamReader(inputStream));
+ }
+
+ InputStream createInputStream(@NotNull String filepath) throws IOException {
+ return new BufferedInputStream(new FileInputStream(filepath));
+ }
+}
@Autowired
ApplicationConfig applicationConfig;
+ @Autowired
+ RefreshConfigTask refreshConfigTask;
+
@Autowired
private Rics rics;
private Services services;
// Only for unittesting
- StartupService(ApplicationConfig appConfig, Rics rics, PolicyTypes policyTypes, A1Client a1Client,
- Policies policies, Services services) {
+ StartupService(ApplicationConfig appConfig, RefreshConfigTask refreshTask, Rics rics, PolicyTypes policyTypes,
+ A1Client a1Client, Policies policies, Services services) {
this.applicationConfig = appConfig;
+ this.refreshConfigTask = refreshTask;
this.rics = rics;
this.policyTypes = policyTypes;
this.a1Client = a1Client;
public void startup() {
logger.debug("Starting up");
applicationConfig.addObserver(this);
- applicationConfig.initialize();
+ refreshConfigTask.start();
}
}
public static class MockApplicationConfig extends ApplicationConfig {
@Override
- protected String getLocalConfigurationFilePath() {
+ public String getLocalConfigurationFilePath() {
URL url = MockApplicationConfig.class.getClassLoader().getResource("test_application_configuration.json");
return url.getFile();
}
static class MockApplicationConfig extends ApplicationConfig {
@Override
- protected String getLocalConfigurationFilePath() {
+ public String getLocalConfigurationFilePath() {
URL url = MockApplicationConfig.class.getClassLoader().getResource("test_application_configuration.json");
return url.getFile();
}
* ========================LICENSE_END===================================
*/
-package org.oransc.policyagent.configuration;
+package org.oransc.policyagent.tasks;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.api.CbsClient;
import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.model.EnvProperties;
import org.onap.dcaegen2.services.sdk.rest.services.cbs.client.model.ImmutableEnvProperties;
+import org.oransc.policyagent.configuration.ApplicationConfig;
+import org.oransc.policyagent.configuration.ApplicationConfigParser;
+import org.oransc.policyagent.configuration.ImmutableRicConfig;
+import org.oransc.policyagent.configuration.RicConfig;
import org.oransc.policyagent.exceptions.ServiceException;
import org.oransc.policyagent.utils.LoggingUtils;
@ExtendWith(MockitoExtension.class)
@RunWith(MockitoJUnitRunner.class)
-public class ApplicationConfigTest {
+public class RefreshConfigTaskTest {
- private ApplicationConfig appConfigUnderTest;
+ private RefreshConfigTask refreshTaskUnderTest;
+
+ @Spy
+ ApplicationConfig appConfig;
@Mock
CbsClient cbsClient;
@Test
public void whenTheConfigurationFits() throws IOException, ServiceException {
-
- appConfigUnderTest = spy(ApplicationConfig.class);
- appConfigUnderTest.systemEnvironment = new Properties();
+ refreshTaskUnderTest = spy(new RefreshConfigTask(appConfig));
+ refreshTaskUnderTest.systemEnvironment = new Properties();
// When
- doReturn(getCorrectJson()).when(appConfigUnderTest).createInputStream(any());
- doReturn("fileName").when(appConfigUnderTest).getLocalConfigurationFilePath();
- appConfigUnderTest.initialize();
+ doReturn(getCorrectJson()).when(refreshTaskUnderTest).createInputStream(any());
+ doReturn("fileName").when(appConfig).getLocalConfigurationFilePath();
+ refreshTaskUnderTest.start();
// Then
- verify(appConfigUnderTest, times(1)).loadConfigurationFromFile();
+ verify(refreshTaskUnderTest, times(1)).loadConfigurationFromFile();
- Iterable<RicConfig> ricConfigs = appConfigUnderTest.getRicConfigs();
+ Iterable<RicConfig> ricConfigs = appConfig.getRicConfigs();
RicConfig ricConfig = ricConfigs.iterator().next();
assertThat(ricConfigs).isNotNull();
assertThat(ricConfig).isEqualTo(CORRECT_RIC_CONIFG);
@Test
public void whenFileIsExistsButJsonIsIncorrect() throws IOException, ServiceException {
-
- appConfigUnderTest = spy(ApplicationConfig.class);
- appConfigUnderTest.systemEnvironment = new Properties();
+ refreshTaskUnderTest = spy(new RefreshConfigTask(appConfig));
+ refreshTaskUnderTest.systemEnvironment = new Properties();
// When
- doReturn(getIncorrectJson()).when(appConfigUnderTest).createInputStream(any());
- doReturn("fileName").when(appConfigUnderTest).getLocalConfigurationFilePath();
- appConfigUnderTest.loadConfigurationFromFile();
+ doReturn(getIncorrectJson()).when(refreshTaskUnderTest).createInputStream(any());
+ doReturn("fileName").when(appConfig).getLocalConfigurationFilePath();
+ refreshTaskUnderTest.loadConfigurationFromFile();
// Then
- verify(appConfigUnderTest, times(1)).loadConfigurationFromFile();
- Assertions.assertEquals(0, appConfigUnderTest.getRicConfigs().size());
+ verify(refreshTaskUnderTest, times(1)).loadConfigurationFromFile();
+ Assertions.assertEquals(0, appConfig.getRicConfigs().size());
}
@Test
public void whenPeriodicConfigRefreshNoEnvironmentVariables() {
+ refreshTaskUnderTest = spy(new RefreshConfigTask(appConfig));
+ refreshTaskUnderTest.systemEnvironment = new Properties();
- appConfigUnderTest = spy(ApplicationConfig.class);
- appConfigUnderTest.systemEnvironment = new Properties();
-
- final ListAppender<ILoggingEvent> logAppender = LoggingUtils.getLogListAppender(ApplicationConfig.class);
- Flux<ApplicationConfig> task = appConfigUnderTest.createRefreshTask();
+ final ListAppender<ILoggingEvent> logAppender = LoggingUtils.getLogListAppender(RefreshConfigTask.class);
+ Flux<ApplicationConfig> task = refreshTaskUnderTest.createRefreshTask();
StepVerifier.create(task).expectSubscription().verifyComplete();
@Test
public void whenPeriodicConfigRefreshNoConsul() {
- appConfigUnderTest = spy(ApplicationConfig.class);
- appConfigUnderTest.systemEnvironment = new Properties();
+ refreshTaskUnderTest = spy(new RefreshConfigTask(appConfig));
+ refreshTaskUnderTest.systemEnvironment = new Properties();
EnvProperties props = properties();
- doReturn(Mono.just(props)).when(appConfigUnderTest).getEnvironment(any());
+ doReturn(Mono.just(props)).when(refreshTaskUnderTest).getEnvironment(any());
- doReturn(Mono.just(cbsClient)).when(appConfigUnderTest).createCbsClient(props);
+ doReturn(Mono.just(cbsClient)).when(refreshTaskUnderTest).createCbsClient(props);
Flux<JsonObject> err = Flux.error(new IOException());
doReturn(err).when(cbsClient).updates(any(), any(), any());
- final ListAppender<ILoggingEvent> logAppender = LoggingUtils.getLogListAppender(ApplicationConfig.class);
- Flux<ApplicationConfig> task = appConfigUnderTest.createRefreshTask();
+ final ListAppender<ILoggingEvent> logAppender = LoggingUtils.getLogListAppender(RefreshConfigTask.class);
+ Flux<ApplicationConfig> task = refreshTaskUnderTest.createRefreshTask();
StepVerifier //
.create(task) //
@Test
public void whenPeriodicConfigRefreshSuccess() throws JsonIOException, JsonSyntaxException, IOException {
- appConfigUnderTest = spy(ApplicationConfig.class);
- appConfigUnderTest.systemEnvironment = new Properties();
+ refreshTaskUnderTest = spy(new RefreshConfigTask(appConfig));
+ refreshTaskUnderTest.systemEnvironment = new Properties();
EnvProperties props = properties();
- doReturn(Mono.just(props)).when(appConfigUnderTest).getEnvironment(any());
- doReturn(Mono.just(cbsClient)).when(appConfigUnderTest).createCbsClient(props);
+ doReturn(Mono.just(props)).when(refreshTaskUnderTest).getEnvironment(any());
+ doReturn(Mono.just(cbsClient)).when(refreshTaskUnderTest).createCbsClient(props);
Flux<JsonObject> json = Flux.just(getJsonRootObject());
doReturn(json).when(cbsClient).updates(any(), any(), any());
- Flux<ApplicationConfig> task = appConfigUnderTest.createRefreshTask();
+ Flux<ApplicationConfig> task = refreshTaskUnderTest.createRefreshTask();
StepVerifier //
.create(task) //
.expectSubscription() //
- .expectNext(appConfigUnderTest) //
+ .expectNext(appConfig) //
.verifyComplete();
- Assertions.assertNotNull(appConfigUnderTest.getRicConfigs());
+ Assertions.assertNotNull(appConfig.getRicConfigs());
}
private JsonObject getJsonRootObject() throws JsonIOException, JsonSyntaxException, IOException {
" \"ric\": {"; //
return new ByteArrayInputStream((string.getBytes(StandardCharsets.UTF_8)));
}
-
}
@Mock
ApplicationConfig appConfigMock;
+ @Mock
+ RefreshConfigTask refreshTaskMock;
+
@Mock
A1Client a1ClientMock;
Rics rics = new Rics();
PolicyTypes policyTypes = new PolicyTypes();
- StartupService serviceUnderTest =
- new StartupService(appConfigMock, rics, policyTypes, a1ClientMock, new Policies(), new Services());
+ StartupService serviceUnderTest = new StartupService(appConfigMock, refreshTaskMock, rics, policyTypes,
+ a1ClientMock, new Policies(), new Services());
serviceUnderTest.startup();
Rics rics = new Rics();
PolicyTypes policyTypes = new PolicyTypes();
- StartupService serviceUnderTest =
- new StartupService(appConfigMock, rics, policyTypes, a1ClientMock, new Policies(), new Services());
+ StartupService serviceUnderTest = new StartupService(appConfigMock, refreshTaskMock, rics, policyTypes,
+ a1ClientMock, new Policies(), new Services());
serviceUnderTest.startup();
serviceUnderTest.onRicConfigUpdate(getRicConfig(FIRST_RIC_NAME, FIRST_RIC_URL, MANAGED_NODE_A),
doReturn(error).when(a1ClientMock).getPolicyIdentities(anyString());
Rics rics = new Rics();
- StartupService serviceUnderTest =
- new StartupService(appConfigMock, rics, new PolicyTypes(), a1ClientMock, new Policies(), new Services());
+ StartupService serviceUnderTest = new StartupService(appConfigMock, refreshTaskMock, rics, new PolicyTypes(),
+ a1ClientMock, new Policies(), new Services());
serviceUnderTest.startup();
serviceUnderTest.onRicConfigUpdate(getRicConfig(FIRST_RIC_NAME, FIRST_RIC_URL, MANAGED_NODE_A),