You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by be...@apache.org on 2018/11/13 12:45:32 UTC

[ambari] branch trunk updated: AMBARI-24881 Add Service Request JSON (benyoka) (#2594)

This is an automated email from the ASF dual-hosted git repository.

benyoka pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 664d14d  AMBARI-24881 Add Service Request JSON (benyoka) (#2594)
664d14d is described below

commit 664d14dae54655a092a304400944bae7e24d1275
Author: benyoka <be...@users.noreply.github.com>
AuthorDate: Tue Nov 13 13:45:28 2018 +0100

    AMBARI-24881 Add Service Request JSON (benyoka) (#2594)
    
    * AMBARI-24881 Add Service Request JSON (benyoka)
    
    * AMBARI-24881 review comments (benyoka)
---
 .../server/controller/AddServiceRequest.java       | 250 ++++++++++++++++++++
 .../ambari/server/topology/Configurable.java       | 147 ++++++++++++
 .../server/controller/AddServiceRequestTest.java   | 263 +++++++++++++++++++++
 .../ambari/server/topology/ConfigurableTest.java   | 104 ++++++++
 .../resources/add_service_api/configurable.json    |  16 ++
 .../resources/add_service_api/configurable2.json   |   8 +
 .../test/resources/add_service_api/request1.json   |  39 +++
 .../test/resources/add_service_api/request2.json   |  18 ++
 .../test/resources/add_service_api/request3.json   |   6 +
 .../test/resources/add_service_api/request4.json   |  13 +
 .../add_service_api/request_invalid_1.json         |  23 ++
 .../add_service_api/request_invalid_2.json         |  40 ++++
 12 files changed, 927 insertions(+)

diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
new file mode 100644
index 0000000..83a66d8
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.server.controller;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Collections.emptySet;
+import static org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
+import static org.apache.ambari.server.controller.internal.ProvisionClusterRequest.CONFIG_RECOMMENDATION_STRATEGY;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.ambari.annotations.ApiIgnore;
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+import org.apache.ambari.server.topology.ConfigRecommendationStrategy;
+import org.apache.ambari.server.topology.Configurable;
+import org.apache.ambari.server.topology.Configuration;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Data object representing an add service request.
+ */
+@ApiModel
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public final class AddServiceRequest {
+
+  static final String OPERATION_TYPE = "operation_type";
+  static final String STACK_NAME = "stack_name";
+  static final String STACK_VERSION = "stack_version";
+  static final String SERVICES = "services";
+  static final String COMPONENTS = "components";
+
+  private final OperationType operationType;
+  private final ConfigRecommendationStrategy recommendationStrategy;
+  private final ProvisionAction provisionAction;
+  private final String stackName;
+  private final String stackVersion;
+  private final Set<Service> services;
+  private final Set<Component> components;
+  private final Configuration configuration;
+
+  @JsonCreator
+  public AddServiceRequest(@JsonProperty(OPERATION_TYPE) OperationType operationType,
+                           @JsonProperty(CONFIG_RECOMMENDATION_STRATEGY) ConfigRecommendationStrategy recommendationStrategy,
+                           @JsonProperty(PROVISION_ACTION_PROPERTY)ProvisionAction provisionAction,
+                           @JsonProperty(STACK_NAME) String stackName,
+                           @JsonProperty(STACK_VERSION) String stackVersion,
+                           @JsonProperty(SERVICES) Set<Service> services,
+                           @JsonProperty(COMPONENTS)Set<Component> components,
+                           @JsonProperty(Configurable.CONFIGURATIONS) Collection<? extends Map<String, ?>> configs) {
+    this(operationType, recommendationStrategy, provisionAction, stackName, stackVersion, services, components,
+      Configurable.parseConfigs(configs));
+  }
+
+
+  private AddServiceRequest(OperationType operationType,
+                            ConfigRecommendationStrategy recommendationStrategy,
+                            ProvisionAction provisionAction,
+                            String stackName,
+                            String stackVersion,
+                            Set<Service> services,
+                            Set<Component> components,
+                            Configuration configuration) {
+    this.operationType = null != operationType ? operationType : OperationType.ADD_SERVICE;
+    this.recommendationStrategy = null != recommendationStrategy ? recommendationStrategy : ConfigRecommendationStrategy.NEVER_APPLY;
+    this.provisionAction = null != provisionAction ? provisionAction : ProvisionAction.INSTALL_AND_START;
+    this.stackName = stackName;
+    this.stackVersion = stackVersion;
+    this.services = null != services ? services : emptySet();
+    this.components = null != components ? components : emptySet();
+    this.configuration = null != configuration ? configuration : new Configuration(new HashMap<>(), new HashMap<>());
+
+    checkArgument(!this.services.isEmpty() || !this.components.isEmpty(), "Either services or components must be specified");
+  }
+
+
+  @JsonProperty(OPERATION_TYPE)
+  @ApiModelProperty(name = OPERATION_TYPE)
+  public OperationType getOperationType() {
+    return operationType;
+  }
+
+  @JsonProperty(CONFIG_RECOMMENDATION_STRATEGY)
+  @ApiModelProperty(name = CONFIG_RECOMMENDATION_STRATEGY)
+  public ConfigRecommendationStrategy getRecommendationStrategy() {
+    return recommendationStrategy;
+  }
+
+  @JsonProperty(PROVISION_ACTION_PROPERTY)
+  @ApiModelProperty(name = PROVISION_ACTION_PROPERTY)
+  public ProvisionAction getProvisionAction() {
+    return provisionAction;
+  }
+
+  @JsonProperty(STACK_NAME)
+  @ApiModelProperty(name = STACK_NAME)
+  public String getStackName() {
+    return stackName;
+  }
+
+  @JsonProperty(STACK_VERSION)
+  @ApiModelProperty(name = STACK_VERSION)
+  public String getStackVersion() {
+    return stackVersion;
+  }
+
+  @JsonProperty(SERVICES)
+  @ApiModelProperty(name = SERVICES)
+  public Set<Service> getServices() {
+    return services;
+  }
+
+  @JsonProperty(COMPONENTS)
+  @ApiModelProperty(name = COMPONENTS)
+  public Set<Component> getComponents() {
+    return components;
+  }
+
+  @JsonIgnore
+  @ApiIgnore
+  public Configuration getConfiguration() {
+    return configuration;
+  }
+
+  @JsonProperty(Configurable.CONFIGURATIONS)
+  @ApiModelProperty(name = Configurable.CONFIGURATIONS)
+  public Collection<Map<String, Map<String, ?>>> getConfigurationContents() {
+    return Configurable.convertConfigToMap(configuration);
+  }
+
+// ------- inner classes -------
+
+  public enum OperationType {
+    ADD_SERVICE, DELETE_SERVICE, MOVE_SERVICE
+  }
+
+  public static class Component {
+    static final String COMPONENT_NAME = "component_name";
+    static final String FQDN = "fqdn";
+
+    private String name;
+    private String fqdn;
+
+    public static final Component of(String name, String fqdn) {
+      Component component = new Component();
+      component.setName(name);
+      component.setFqdn(fqdn);
+      return component;
+    }
+
+    @JsonProperty(COMPONENT_NAME)
+    @ApiModelProperty(name = COMPONENT_NAME)
+    public String getName() {
+      return name;
+    }
+
+    @JsonProperty(COMPONENT_NAME)
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    @JsonProperty(FQDN)
+    @ApiModelProperty(name = FQDN)
+    public String getFqdn() {
+      return fqdn;
+    }
+
+    @JsonProperty(FQDN)
+    public void setFqdn(String fqdn) {
+      this.fqdn = fqdn;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (o == null || getClass() != o.getClass()) return false;
+      Component component = (Component) o;
+      return Objects.equals(name, component.name) &&
+        Objects.equals(fqdn, component.fqdn);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, fqdn);
+    }
+  }
+
+  @ApiModel
+  public static class Service {
+    static final String NAME = "name";
+
+    private String name;
+
+    public static final Service of(String name) {
+      Service service = new Service();
+      service.setName(name);
+      return service;
+    }
+
+    @JsonProperty(NAME)
+    @ApiModelProperty(name = NAME)
+    public String getName() {
+      return name;
+    }
+
+    @JsonProperty(NAME)
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (o == null || getClass() != o.getClass()) return false;
+      Service service = (Service) o;
+      return Objects.equals(name, service.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name);
+    }
+  }
+}
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/Configurable.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/Configurable.java
new file mode 100644
index 0000000..e8f780d
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/Configurable.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.server.topology;
+
+import static org.apache.ambari.server.controller.internal.BlueprintResourceProvider.PROPERTIES_ATTRIBUTES_PROPERTY_ID;
+import static org.apache.ambari.server.controller.internal.BlueprintResourceProvider.PROPERTIES_PROPERTY_ID;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.apache.ambari.annotations.ApiIgnore;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Provides support for JSON serializaion of {@link Configuration} objects. Can handle both plain JSON and Ambari style
+ * flattened JSON such as {@code "hdfs-site/properties/dfs.replication": "3"}. Objects may implement this interface or
+ * call its static utility methods.
+ */
+public interface Configurable {
+
+  public static final String CONFIGURATIONS = "configurations";
+
+  @JsonIgnore
+  @ApiIgnore
+  void setConfiguration(Configuration configuration);
+
+  @JsonIgnore
+  @ApiIgnore
+  Configuration getConfiguration();
+
+  @JsonProperty(CONFIGURATIONS)
+  @ApiModelProperty(name = CONFIGURATIONS)
+  default void setConfigs(Collection<? extends Map<String, ?>> configs) {
+    setConfiguration(parseConfigs(configs));
+  }
+
+  @JsonProperty(CONFIGURATIONS)
+  @ApiModelProperty(name = CONFIGURATIONS)
+  default Collection<Map<String, Map<String, ?>>> getConfigs() {
+    return convertConfigToMap(getConfiguration());
+  }
+
+  /**
+   * Parses configuration maps The configs can be in fully structured JSON, e.g.
+   * <code>
+   * [{"hdfs-site":
+   *  "properties": {
+   *    ""dfs.replication": "3",
+   *    ...
+   *  },
+   *  properties_attributes: {}
+   * }]
+   * </code>
+   * or flattened like
+   * <code>
+   * [{
+   *  "hdfs-site/properties/dfs.replication": "3",
+   *  ...
+   * }]
+   * </code>
+   * In the latter case it calls {@link ConfigurationFactory#getConfiguration(Collection)}
+   * @param configs
+   * @return
+   */
+  static Configuration parseConfigs(@Nullable Collection<? extends Map<String, ?>> configs) {
+    Configuration configuration;
+
+    if (null == configs) {
+      configuration = new Configuration(new HashMap<>(), new HashMap<>());
+    }
+    else if (!configs.isEmpty() && configs.iterator().next().keySet().iterator().next().contains("/")) {
+      // Configuration has keys with slashes like "zk.cfg/properties/dataDir" means it is coming through
+      // the resource framework and must be parsed with configuration factories
+      configuration = new ConfigurationFactory().getConfiguration((Collection<Map<String, String>>)configs);
+    }
+    else {
+      // If the configuration does not have keys with slashes it means it is coming from plain JSON and needs to be
+      // parsed accordingly.
+      Map<String, Map<String, String>> allProperties = new HashMap<>();
+      Map<String, Map<String, Map<String, String>>> allAttributes = new HashMap<>();
+      configs.forEach( item -> {
+        String configName = item.keySet().iterator().next();
+        Map<String, Object> configData = (Map<String, Object>) item.get(configName);
+        if (configData.containsKey(PROPERTIES_PROPERTY_ID)) {
+          Map<String, String> properties = (Map<String, String>)configData.get(PROPERTIES_PROPERTY_ID);
+          allProperties.put(configName, properties);
+        }
+        if (configData.containsKey(PROPERTIES_ATTRIBUTES_PROPERTY_ID)) {
+          Map<String, Map<String, String>> attributes =
+            (Map<String, Map<String, String>>)configData.get(PROPERTIES_ATTRIBUTES_PROPERTY_ID);
+          allAttributes.put(configName, attributes);
+        }
+      });
+      configuration = new Configuration(allProperties, allAttributes);
+    }
+    return configuration;
+  }
+
+  /**
+   * Converts {@link Configuration} objects to a collection easily serializable to Json
+   * @param configuration the configuration to convert
+   * @return the resulting collection
+   */
+  static Collection<Map<String, Map<String, ?>>> convertConfigToMap(Configuration configuration) {
+    Collection<Map<String, Map<String, ?>>> configurations = new ArrayList<>();
+    Set<String> allConfigTypes = Sets.union(configuration.getProperties().keySet(), configuration.getAttributes().keySet());
+    for (String configType: allConfigTypes) {
+      Map<String, Map<String, ? extends Object>> configData = new HashMap<>();
+      if (configuration.getProperties().containsKey(configType)) {
+        configData.put(PROPERTIES_PROPERTY_ID, configuration.getProperties().get(configType));
+      }
+      if (configuration.getAttributes().containsKey(configType)) {
+        configData.put(PROPERTIES_ATTRIBUTES_PROPERTY_ID, configuration.getAttributes().get(configType));
+      }
+      configurations.add(ImmutableMap.of(configType, configData));
+    }
+    return configurations;
+  }
+
+}
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
new file mode 100644
index 0000000..2ed98b7
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.server.controller;
+
+import static org.apache.ambari.server.controller.AddServiceRequest.COMPONENTS;
+import static org.apache.ambari.server.controller.AddServiceRequest.Component;
+import static org.apache.ambari.server.controller.AddServiceRequest.OperationType.ADD_SERVICE;
+import static org.apache.ambari.server.controller.AddServiceRequest.SERVICES;
+import static org.apache.ambari.server.controller.AddServiceRequest.STACK_NAME;
+import static org.apache.ambari.server.controller.AddServiceRequest.STACK_VERSION;
+import static org.apache.ambari.server.controller.AddServiceRequest.Service;
+import static org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
+import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_AND_START;
+import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_ONLY;
+import static org.apache.ambari.server.controller.internal.ProvisionClusterRequest.CONFIG_RECOMMENDATION_STRATEGY;
+import static org.apache.ambari.server.serveraction.kerberos.KerberosServerAction.OPERATION_TYPE;
+import static org.apache.ambari.server.topology.ConfigRecommendationStrategy.ALWAYS_APPLY;
+import static org.apache.ambari.server.topology.ConfigRecommendationStrategy.NEVER_APPLY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+import org.apache.ambari.server.topology.ConfigRecommendationStrategy;
+import org.apache.ambari.server.topology.Configurable;
+import org.apache.ambari.server.topology.Configuration;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Resources;
+
+/**
+ * Tests for {@link AddServiceRequest} serialization / deserialization / syntax validation
+ */
+public class AddServiceRequestTest {
+
+  private static String REQUEST_ALL_FIELDS_SET;
+  private static String REQUEST_MINIMAL_SERVICES_AND_COMPONENTS;
+  private static String REQUEST_MINIMAL_SERVICES_ONLY;
+  private static String REQUEST_MINIMAL_COMPONENTS_ONLY;
+  private static String REQUEST_INVALID_NO_SERVICES_AND_COMPONENTS;
+  private static String REQUEST_INVALID_INVALID_FIELD;
+
+
+  private ObjectMapper mapper = new ObjectMapper();
+
+  @BeforeClass
+  public static void setUpClass() {
+    REQUEST_ALL_FIELDS_SET = read("add_service_api/request1.json");
+    REQUEST_MINIMAL_SERVICES_AND_COMPONENTS = read("add_service_api/request2.json");
+    REQUEST_MINIMAL_SERVICES_ONLY = read("add_service_api/request3.json");
+    REQUEST_MINIMAL_COMPONENTS_ONLY = read("add_service_api/request4.json");
+    REQUEST_INVALID_NO_SERVICES_AND_COMPONENTS = read("add_service_api/request_invalid_1.json");
+    REQUEST_INVALID_INVALID_FIELD = read("add_service_api/request_invalid_2.json");
+  }
+
+  @Test
+  public void testDeserialize_basic() throws Exception {
+    AddServiceRequest request = mapper.readValue(REQUEST_ALL_FIELDS_SET, AddServiceRequest.class);
+
+    assertEquals(ADD_SERVICE, request.getOperationType());
+    assertEquals(ALWAYS_APPLY, request.getRecommendationStrategy());
+    assertEquals(INSTALL_ONLY, request.getProvisionAction());
+    assertEquals("HDP", request.getStackName());
+    assertEquals("3.0", request.getStackVersion());
+
+    Configuration configuration = request.getConfiguration();
+    assertEquals(
+      ImmutableMap.of("storm-site", ImmutableMap.of("final", ImmutableMap.of("fs.defaultFS", "true"))),
+      configuration.getAttributes());
+    assertEquals(
+      ImmutableMap.of("storm-site", ImmutableMap.of("ipc.client.connect.max.retries", "50")),
+      configuration.getProperties());
+
+    assertEquals(
+      ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org"), Component.of("BEACON_SERVER", "c7402.ambari.apache.org")),
+      request.getComponents());
+
+    assertEquals(
+      ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+      request.getServices());
+
+  }
+
+  @Test
+  public void testDeserialize_defaultAndEmptyValues() throws Exception {
+    AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_SERVICES_AND_COMPONENTS, AddServiceRequest.class);
+
+    // filled-out values
+    assertEquals(
+      ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org"), Component.of("BEACON_SERVER", "c7402.ambari.apache.org")),
+      request.getComponents());
+
+    assertEquals(
+      ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+      request.getServices());
+
+    // default / empty values
+    assertEquals(ADD_SERVICE, request.getOperationType());
+    assertEquals(NEVER_APPLY, request.getRecommendationStrategy());
+    assertEquals(INSTALL_AND_START, request.getProvisionAction());
+    assertNull(request.getStackName());
+    assertNull(request.getStackVersion());
+
+    Configuration configuration = request.getConfiguration();
+    assertTrue(configuration.getFullAttributes().isEmpty());
+    assertTrue(configuration.getFullProperties().isEmpty());
+  }
+
+  @Test
+  public void testDeserialize_onlyServices() throws Exception {
+    AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_SERVICES_ONLY, AddServiceRequest.class);
+
+    // filled-out values
+    assertEquals(
+      ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+      request.getServices());
+
+    // default / empty values
+    assertEquals(ADD_SERVICE, request.getOperationType());
+    assertEquals(NEVER_APPLY, request.getRecommendationStrategy());
+    assertEquals(INSTALL_AND_START, request.getProvisionAction());
+    assertNull(request.getStackName());
+    assertNull(request.getStackVersion());
+
+    Configuration configuration = request.getConfiguration();
+    assertTrue(configuration.getFullAttributes().isEmpty());
+    assertTrue(configuration.getFullProperties().isEmpty());
+
+    assertTrue(request.getComponents().isEmpty());
+  }
+
+  @Test
+  public void testDeserialize_onlyComponents() throws Exception {
+    AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_COMPONENTS_ONLY, AddServiceRequest.class);
+
+    // filled-out values
+    assertEquals(
+      ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org"), Component.of("BEACON_SERVER", "c7402.ambari.apache.org")),
+      request.getComponents());
+
+    // default / empty values
+    assertEquals(ADD_SERVICE, request.getOperationType());
+    assertEquals(NEVER_APPLY, request.getRecommendationStrategy());
+    assertEquals(INSTALL_AND_START, request.getProvisionAction());
+    assertNull(request.getStackName());
+    assertNull(request.getStackVersion());
+
+    Configuration configuration = request.getConfiguration();
+    assertTrue(configuration.getFullAttributes().isEmpty());
+    assertTrue(configuration.getFullProperties().isEmpty());
+
+    assertTrue(request.getServices().isEmpty());
+  }
+
+  @Test(expected = JsonProcessingException.class)
+  public void testDeserialize_invalid_noServicesAndComponents() throws Exception {
+    mapper.readValue(REQUEST_INVALID_NO_SERVICES_AND_COMPONENTS, AddServiceRequest.class);
+  }
+
+  @Test(expected = JsonProcessingException.class)
+  public void testDeserialize_invalid_invalidField() throws Exception {
+    mapper.readValue(REQUEST_INVALID_INVALID_FIELD, AddServiceRequest.class);
+  }
+
+  @Test
+  public void testSerialize_basic() throws Exception {
+    AddServiceRequest request = mapper.readValue(REQUEST_ALL_FIELDS_SET, AddServiceRequest.class);
+
+    Map<String, ?> serialized = serialize(request);
+
+    assertEquals(AddServiceRequest.OperationType.ADD_SERVICE.name(), serialized.get(OPERATION_TYPE));
+    assertEquals(ConfigRecommendationStrategy.ALWAYS_APPLY.name(), serialized.get(CONFIG_RECOMMENDATION_STRATEGY));
+    assertEquals(ProvisionAction.INSTALL_ONLY.name(), serialized.get(PROVISION_ACTION_PROPERTY));
+    assertEquals("HDP", serialized.get(STACK_NAME));
+    assertEquals("3.0", serialized.get(STACK_VERSION));
+
+    assertEquals(
+      ImmutableSet.of(ImmutableMap.of(Service.NAME, "BEACON"), ImmutableMap.of(Service.NAME, "STORM")),
+      ImmutableSet.copyOf((List<String>) serialized.get(SERVICES)) );
+
+    assertEquals(
+      ImmutableSet.of(
+        ImmutableMap.of(Component.COMPONENT_NAME, "NIMBUS", Component.FQDN, "c7401.ambari.apache.org"),
+        ImmutableMap.of(Component.COMPONENT_NAME, "BEACON_SERVER", Component.FQDN, "c7402.ambari.apache.org")),
+      ImmutableSet.copyOf((List<String>) serialized.get(COMPONENTS)) );
+
+    assertEquals(
+      ImmutableList.of(
+        ImmutableMap.of(
+          "storm-site",
+          ImmutableMap.of(
+            "properties", ImmutableMap.of("ipc.client.connect.max.retries", "50"),
+            "properties_attributes", ImmutableMap.of("final", ImmutableMap.of("fs.defaultFS", "true"))
+          )
+        )
+      ),
+      serialized.get(Configurable.CONFIGURATIONS)
+    );
+  }
+
+  @Test
+  public void testSerialize_EmptyOmitted() throws Exception {
+    AddServiceRequest request = mapper.readValue(REQUEST_MINIMAL_SERVICES_ONLY, AddServiceRequest.class);
+    Map<String, ?> serialized = serialize(request);
+
+    assertEquals(AddServiceRequest.OperationType.ADD_SERVICE.name(), serialized.get(OPERATION_TYPE));
+    assertEquals(ProvisionAction.INSTALL_AND_START.name(), serialized.get(PROVISION_ACTION_PROPERTY));
+    assertEquals(
+      ImmutableSet.of(ImmutableMap.of(Service.NAME, "BEACON"), ImmutableMap.of(Service.NAME, "STORM")),
+      ImmutableSet.copyOf((List<String>) serialized.get(SERVICES)) );
+
+    assertFalse(serialized.containsKey(STACK_NAME));
+    assertFalse(serialized.containsKey(STACK_VERSION));
+    assertFalse(serialized.containsKey(Configurable.CONFIGURATIONS));
+    assertFalse(serialized.containsKey(COMPONENTS));
+
+  }
+
+  private Map<String, ?> serialize(AddServiceRequest request) throws IOException {
+    String serialized = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
+    return mapper.readValue(serialized, new TypeReference<Map<String, ?>>() {});
+  }
+
+  private static String read(String resourceName) {
+    try {
+      return Resources.toString(Resources.getResource(resourceName), StandardCharsets.UTF_8);
+    }
+    catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/topology/ConfigurableTest.java b/ambari-server/src/test/java/org/apache/ambari/server/topology/ConfigurableTest.java
new file mode 100644
index 0000000..69aec1f
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/topology/ConfigurableTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+package org.apache.ambari.server.topology;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URL;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Resources;
+
+public class ConfigurableTest {
+  public static final String JSON_LOCATION = "add_service_api/configurable.json";
+  public static final String JSON_LOCATION2 = "add_service_api/configurable2.json";
+
+  private TestConfigurable configurable;
+  private ObjectMapper mapper;
+
+  @Before
+  public void setUp() throws Exception {
+    mapper = new ObjectMapper();
+    URL url = Resources.getResource(JSON_LOCATION);
+    configurable = new ObjectMapper().readValue(url, TestConfigurable.class);
+  }
+
+  /**
+   * Parse normal JSON configuration
+   */
+  @Test
+  public void testParseConfigurable() throws Exception {
+    assertEquals(ImmutableMap.of("zoo.cfg", ImmutableMap.of("dataDir", "/zookeeper1")),
+      configurable.getConfiguration().getProperties());
+    assertEquals(
+      ImmutableMap.of("zoo.cfg",
+        ImmutableMap.of("final",
+          ImmutableMap.of("someProp", "true"))),
+      configurable.getConfiguration().getAttributes());
+  }
+
+  /**
+   * Deserialize normal JSON configuration
+   */
+  @Test
+  public void testSerializaDeserialize() throws Exception {
+    String persisted = mapper.writeValueAsString(configurable);
+    Configurable restored = mapper.readValue(persisted, TestConfigurable.class);
+    assertEquals(configurable.getConfiguration().getProperties(), restored.getConfiguration().getProperties());
+    assertEquals(configurable.getConfiguration().getAttributes(), restored.getConfiguration().getAttributes());
+  }
+
+  /**
+   * Parse flattened configuration
+   */
+  @Test
+  public void testParseConfigurableFromResoueceManager() throws Exception{
+    mapper = new ObjectMapper();
+    URL url = Resources.getResource(JSON_LOCATION2);
+    configurable = new ObjectMapper().readValue(url, TestConfigurable.class);
+
+    assertEquals(ImmutableMap.of("zoo.cfg", ImmutableMap.of("dataDir", "/zookeeper1")),
+      configurable.getConfiguration().getProperties());
+    assertEquals(
+      ImmutableMap.of("zoo.cfg",
+        ImmutableMap.of("final",
+          ImmutableMap.of("someProp", "true"))),
+      configurable.getConfiguration().getAttributes());
+  }
+
+}
+
+class TestConfigurable implements Configurable {
+  Configuration configuration;
+
+  @Override
+  public Configuration getConfiguration() {
+    return configuration;
+  }
+
+  @Override
+  public void setConfiguration(Configuration configuration) {
+    this.configuration = configuration;
+  }
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/configurable.json b/ambari-server/src/test/resources/add_service_api/configurable.json
new file mode 100644
index 0000000..cefa23b
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/configurable.json
@@ -0,0 +1,16 @@
+{
+  "configurations" : [
+    {
+      "zoo.cfg" : {
+        "properties" : {
+          "dataDir" : "/zookeeper1"
+        },
+        "properties_attributes": {
+          "final": {
+            "someProp": "true"
+          }
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/configurable2.json b/ambari-server/src/test/resources/add_service_api/configurable2.json
new file mode 100644
index 0000000..c27ecf4
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/configurable2.json
@@ -0,0 +1,8 @@
+{
+  "configurations" : [
+    {
+      "zoo.cfg/properties/dataDir" : "/zookeeper1",
+      "zoo.cfg/properties_attributes/final/someProp": "true"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request1.json b/ambari-server/src/test/resources/add_service_api/request1.json
new file mode 100644
index 0000000..cf4ce62
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request1.json
@@ -0,0 +1,39 @@
+{
+  "operation_type" : "ADD_SERVICE",
+  "config_recommendation_strategy" : "ALWAYS_APPLY",
+  "provision_action" : "INSTALL_ONLY",
+  "stack_name" : "HDP",
+  "stack_version" : "3.0",
+
+  "services": [
+    { "name" : "STORM" },
+    { "name" : "BEACON" }
+  ],
+
+  "components" : [
+    {
+      "component_name" : "NIMBUS",
+      "fqdn" : "c7401.ambari.apache.org"
+    },
+    {
+      "component_name" : "BEACON_SERVER",
+      "fqdn" : "c7402.ambari.apache.org"
+    }
+  ],
+
+  "configurations" : [
+    {
+      "storm-site" : {
+        "properties_attributes" : {
+          "final" : {
+            "fs.defaultFS" : "true"
+          }
+        },
+        "properties" : {
+          "ipc.client.connect.max.retries" : "50"
+        }
+      }
+    }
+  ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request2.json b/ambari-server/src/test/resources/add_service_api/request2.json
new file mode 100644
index 0000000..f0e540f
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request2.json
@@ -0,0 +1,18 @@
+{
+  "services": [
+    { "name" : "STORM" },
+    { "name" : "BEACON" }
+  ],
+
+  "components" : [
+    {
+      "component_name" : "NIMBUS",
+      "fqdn" : "c7401.ambari.apache.org"
+    },
+    {
+      "component_name" : "BEACON_SERVER",
+      "fqdn" : "c7402.ambari.apache.org"
+    }
+  ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request3.json b/ambari-server/src/test/resources/add_service_api/request3.json
new file mode 100644
index 0000000..4e715c1
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request3.json
@@ -0,0 +1,6 @@
+{
+  "services": [
+    { "name" : "STORM" },
+    { "name" : "BEACON" }
+  ]
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request4.json b/ambari-server/src/test/resources/add_service_api/request4.json
new file mode 100644
index 0000000..b6fd0ea
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request4.json
@@ -0,0 +1,13 @@
+{
+  "components" : [
+    {
+      "component_name" : "NIMBUS",
+      "fqdn" : "c7401.ambari.apache.org"
+    },
+    {
+      "component_name" : "BEACON_SERVER",
+      "fqdn" : "c7402.ambari.apache.org"
+    }
+  ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request_invalid_1.json b/ambari-server/src/test/resources/add_service_api/request_invalid_1.json
new file mode 100644
index 0000000..432970f
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request_invalid_1.json
@@ -0,0 +1,23 @@
+{
+  "operation_type" : "ADD_SERVICE",
+  "config_recommendation_strategy" : "ALWAYS_APPLY",
+  "provision_action" : "INSTALL_ONLY",
+  "stack_name" : "HDP",
+  "stack_version" : "3.0",
+
+  "configurations" : [
+    {
+      "storm-site" : {
+        "properties_attributes" : {
+          "final" : {
+            "fs.defaultFS" : "true"
+          }
+        },
+        "properties" : {
+          "ipc.client.connect.max.retries" : "50"
+        }
+      }
+    }
+  ]
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/resources/add_service_api/request_invalid_2.json b/ambari-server/src/test/resources/add_service_api/request_invalid_2.json
new file mode 100644
index 0000000..a017c0b
--- /dev/null
+++ b/ambari-server/src/test/resources/add_service_api/request_invalid_2.json
@@ -0,0 +1,40 @@
+{
+  "mpack" : "HDPCORE-2.0",
+  "operation_type" : "ADD_SERVICE",
+  "config_recommendation_strategy" : "ALWAYS_APPLY",
+  "provision_action" : "INSTALL_ONLY",
+  "stack_name" : "HDP",
+  "stack_version" : "3.0",
+
+  "services": [
+    { "name" : "STORM" },
+    { "name" : "BEACON" }
+  ],
+
+  "components" : [
+    {
+      "component_name" : "NIMBUS",
+      "fqdn" : "c7401.ambari.apache.org"
+    },
+    {
+      "component_name" : "BEACON_SERVER",
+      "fqdn" : "c7402.ambari.apache.org"
+    }
+  ],
+
+  "configurations" : [
+    {
+      "storm-site" : {
+        "properties_attributes" : {
+          "final" : {
+            "fs.defaultFS" : "true"
+          }
+        },
+        "properties" : {
+          "ipc.client.connect.max.retries" : "50"
+        }
+      }
+    }
+  ]
+
+}
\ No newline at end of file