You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by jx...@apache.org on 2020/04/07 01:20:35 UTC

[helix] 06/12: Implement Azure cloud instance information processor (#698)

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

jxue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/helix.git

commit 079483b37eb59b1b10a91b92f30468de7da73547
Author: zhangmeng916 <56...@users.noreply.github.com>
AuthorDate: Fri Feb 7 15:24:29 2020 -0800

    Implement Azure cloud instance information processor (#698)
    
    Implement Azure cloud instance information processor. The processor performs the functions of fetching and parsing instance information from Azure cloud environment. The endpoint that the processor queries is a globally standard url, called Azure Instance Metadata Service.
---
 helix-core/pom.xml                                 |   5 +
 .../cloud/CloudInstanceInformationProcessor.java   |   1 -
 .../cloud/azure/AzureCloudInstanceInformation.java |  11 +-
 .../AzureCloudInstanceInformationProcessor.java    | 113 +++++++++++++++++++--
 .../org/apache/helix/cloud/MockHttpClient.java     |  53 ++++++++++
 ...TestAzureCloudInstanceInformationProcessor.java |  69 +++++++++++++
 helix-core/src/test/resources/AzureResponse.json   | 104 +++++++++++++++++++
 helix-rest/pom.xml                                 |   5 -
 8 files changed, 343 insertions(+), 18 deletions(-)

diff --git a/helix-core/pom.xml b/helix-core/pom.xml
index 18113e1..6aaa816 100644
--- a/helix-core/pom.xml
+++ b/helix-core/pom.xml
@@ -163,6 +163,11 @@ under the License.
       <artifactId>metrics-core</artifactId>
       <version>3.2.3</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+      <version>4.5.8</version>
+    </dependency>
   </dependencies>
   <build>
     <resources>
diff --git a/helix-core/src/main/java/org/apache/helix/api/cloud/CloudInstanceInformationProcessor.java b/helix-core/src/main/java/org/apache/helix/api/cloud/CloudInstanceInformationProcessor.java
index a365777..9ff6b00 100644
--- a/helix-core/src/main/java/org/apache/helix/api/cloud/CloudInstanceInformationProcessor.java
+++ b/helix-core/src/main/java/org/apache/helix/api/cloud/CloudInstanceInformationProcessor.java
@@ -21,7 +21,6 @@ package org.apache.helix.api.cloud;
 
 import java.util.List;
 
-
 /**
  * Generic interface to fetch and parse cloud instance information
  */
diff --git a/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformation.java b/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformation.java
index f7fd657..1fef205 100644
--- a/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformation.java
+++ b/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformation.java
@@ -19,17 +19,17 @@ package org.apache.helix.cloud.azure;
  * under the License.
  */
 
+import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.helix.api.cloud.CloudInstanceInformation;
 
-
 public class AzureCloudInstanceInformation implements CloudInstanceInformation {
   private Map<String, String> _cloudInstanceInfoMap;
 
   /**
    * Instantiate the AzureCloudInstanceInformation using each field individually.
-   * Users should use AzureCloudInstanceInformation.Builder to create information.
+   * Users should use AzureCloudInstanceInformation.Builder to set field information.
    * @param cloudInstanceInfoMap
    */
   protected AzureCloudInstanceInformation(Map<String, String> cloudInstanceInfoMap) {
@@ -42,10 +42,11 @@ public class AzureCloudInstanceInformation implements CloudInstanceInformation {
   }
 
   public static class Builder {
-    private Map<String, String> _cloudInstanceInfoMap = null;
+
+    private final Map<String, String> _cloudInstanceInfoMap = new HashMap<>();
 
     public AzureCloudInstanceInformation build() {
-      return new AzureCloudInstanceInformation(_cloudInstanceInfoMap);
+      return new AzureCloudInstanceInformation(new HashMap<>(_cloudInstanceInfoMap));
     }
 
     public Builder setInstanceName(String name) {
@@ -68,4 +69,4 @@ public class AzureCloudInstanceInformation implements CloudInstanceInformation {
       return this;
     }
   }
-}
\ No newline at end of file
+}
diff --git a/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformationProcessor.java b/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformationProcessor.java
index 84a102c..85e2bb5 100644
--- a/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformationProcessor.java
+++ b/helix-core/src/main/java/org/apache/helix/cloud/azure/AzureCloudInstanceInformationProcessor.java
@@ -19,38 +19,137 @@ package org.apache.helix.cloud.azure;
  * under the License.
  */
 
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.net.ssl.SSLException;
+import org.apache.helix.HelixCloudProperty;
+import org.apache.helix.HelixException;
 import org.apache.helix.api.cloud.CloudInstanceInformationProcessor;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+public class AzureCloudInstanceInformationProcessor
+    implements CloudInstanceInformationProcessor<String> {
+  private static final Logger LOG =
+      LoggerFactory.getLogger(AzureCloudInstanceInformationProcessor.class);
+  private final CloseableHttpClient _closeableHttpClient;
+  private final HelixCloudProperty _helixCloudProperty;
+  private final String COMPUTE = "compute";
+  private final String INSTANCE_NAME = "vmId";
+  private final String DOMAIN = "platformFaultDomain";
+  private final String INSTANCE_SET_NAME = "vmScaleSetName";
 
-public class AzureCloudInstanceInformationProcessor implements CloudInstanceInformationProcessor<String> {
+  public AzureCloudInstanceInformationProcessor(HelixCloudProperty helixCloudProperty) {
+    _helixCloudProperty = helixCloudProperty;
 
-  public AzureCloudInstanceInformationProcessor() {
+    RequestConfig requestConifg = RequestConfig.custom()
+        .setConnectionRequestTimeout((int) helixCloudProperty.getCloudRequestTimeout())
+        .setConnectTimeout((int) helixCloudProperty.getCloudConnectionTimeout()).build();
+
+    HttpRequestRetryHandler httpRequestRetryHandler =
+        (IOException exception, int executionCount, HttpContext context) -> {
+          LOG.warn("Execution count: " + executionCount + ".", exception);
+          return !(executionCount >= helixCloudProperty.getCloudMaxRetry()
+              || exception instanceof InterruptedIOException
+              || exception instanceof UnknownHostException || exception instanceof SSLException);
+        };
+
+    //TODO: we should regularize the way how httpClient should be used throughout Helix. e.g. Helix-rest could also use in the same way
+    _closeableHttpClient = HttpClients.custom().setDefaultRequestConfig(requestConifg)
+        .setRetryHandler(httpRequestRetryHandler).build();
   }
 
   /**
-   * fetch the raw Azure cloud instance information
+   * This constructor is for unit test purpose only.
+   * User could provide helixCloudProperty and a mocked http client to test the functionality of
+   * this class.
+   */
+  public AzureCloudInstanceInformationProcessor(HelixCloudProperty helixCloudProperty,
+      CloseableHttpClient closeableHttpClient) {
+    _helixCloudProperty = helixCloudProperty;
+    _closeableHttpClient = closeableHttpClient;
+  }
+
+  /**
+   * Fetch raw Azure cloud instance information based on the urls provided
    * @return raw Azure cloud instance information
    */
   @Override
   public List<String> fetchCloudInstanceInformation() {
     List<String> response = new ArrayList<>();
-    //TODO: implement the fetching logic
+    for (String url : _helixCloudProperty.getCloudInfoSources()) {
+      response.add(getAzureCloudInformationFromUrl(url));
+    }
     return response;
   }
 
   /**
+   * Query Azure Instance Metadata Service to get the instance(VM) information
+   * @return raw Azure cloud instance information
+   */
+  private String getAzureCloudInformationFromUrl(String url) {
+    HttpGet httpGet = new HttpGet(url);
+    httpGet.setHeader("Metadata", "true");
+
+    try {
+      CloseableHttpResponse response = _closeableHttpClient.execute(httpGet);
+      if (response == null || response.getStatusLine().getStatusCode() != 200) {
+        String errorMsg = String.format(
+            "Failed to get an HTTP Response for the request. Response: {}. Status code: {}",
+            (response == null ? "NULL" : response.getStatusLine().getReasonPhrase()),
+            response.getStatusLine().getStatusCode());
+        throw new HelixException(errorMsg);
+      }
+      String responseString = EntityUtils.toString(response.getEntity());
+      LOG.info("VM instance information query result: {}", responseString);
+      return responseString;
+    } catch (IOException e) {
+      throw new HelixException(
+          String.format("Failed to get Azure cloud instance information from url {}", url), e);
+    }
+  }
+
+  /**
    * Parse raw Azure cloud instance information.
    * @return required azure cloud instance information
    */
   @Override
   public AzureCloudInstanceInformation parseCloudInstanceInformation(List<String> responses) {
     AzureCloudInstanceInformation azureCloudInstanceInformation = null;
-    //TODO: implement the parsing logic
+    if (responses.size() > 1) {
+      throw new HelixException("Multiple responses are not supported for Azure now");
+    }
+    String response = responses.get(0);
+    ObjectMapper mapper = new ObjectMapper();
+    try {
+      JsonNode jsonNode = mapper.readTree(response);
+      JsonNode computeNode = jsonNode.path(COMPUTE);
+      if (!computeNode.isMissingNode()) {
+        String vmName = computeNode.path(INSTANCE_NAME).getTextValue();
+        String platformFaultDomain = computeNode.path(DOMAIN).getTextValue();
+        String vmssName = computeNode.path(INSTANCE_SET_NAME).getValueAsText();
+        AzureCloudInstanceInformation.Builder builder = new AzureCloudInstanceInformation.Builder();
+        builder.setInstanceName(vmName).setFaultDomain(platformFaultDomain)
+            .setInstanceSetName(vmssName);
+        azureCloudInstanceInformation = builder.build();
+      }
+    } catch (IOException e) {
+      throw new HelixException(String.format("Error in parsing cloud instance information: {}", response, e));
+    }
     return azureCloudInstanceInformation;
   }
 }
-
-
diff --git a/helix-core/src/test/java/org/apache/helix/cloud/MockHttpClient.java b/helix-core/src/test/java/org/apache/helix/cloud/MockHttpClient.java
new file mode 100644
index 0000000..03e687e
--- /dev/null
+++ b/helix-core/src/test/java/org/apache/helix/cloud/MockHttpClient.java
@@ -0,0 +1,53 @@
+package org.apache.helix.cloud;
+
+/*
+ * 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.
+ */
+
+import java.io.InputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+
+
+/**
+ * Mock a http client and provide response using resource file. This is for unit test purpose only.
+ */
+public class MockHttpClient {
+  protected CloseableHttpClient createMockHttpClient(String file) throws Exception {
+    InputStream responseInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(file);
+    HttpEntity httpEntity = Mockito.mock(HttpEntity.class);
+    StatusLine statusLine = Mockito.mock(StatusLine.class);
+
+    CloseableHttpResponse mockCloseableHttpResponse = Mockito.mock(CloseableHttpResponse.class);
+    CloseableHttpClient mockCloseableHttpClient = Mockito.mock(CloseableHttpClient.class);
+
+    Mockito.when(httpEntity.getContent()).thenReturn(responseInputStream);
+    Mockito.when(mockCloseableHttpClient.execute(Matchers.any(HttpGet.class))).thenReturn(mockCloseableHttpResponse);
+    Mockito.when(mockCloseableHttpResponse.getEntity()).thenReturn(httpEntity);
+    Mockito.when(mockCloseableHttpResponse.getStatusLine()).thenReturn(statusLine);
+    Mockito.when(statusLine.getStatusCode()).thenReturn(200);
+
+    return mockCloseableHttpClient;
+  }
+}
\ No newline at end of file
diff --git a/helix-core/src/test/java/org/apache/helix/cloud/TestAzureCloudInstanceInformationProcessor.java b/helix-core/src/test/java/org/apache/helix/cloud/TestAzureCloudInstanceInformationProcessor.java
new file mode 100644
index 0000000..10ba42d
--- /dev/null
+++ b/helix-core/src/test/java/org/apache/helix/cloud/TestAzureCloudInstanceInformationProcessor.java
@@ -0,0 +1,69 @@
+package org.apache.helix.cloud;
+
+/*
+ * 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.
+ */
+
+import java.util.List;
+
+import org.apache.helix.HelixCloudProperty;
+import org.apache.helix.api.cloud.CloudInstanceInformation;
+import org.apache.helix.cloud.azure.AzureCloudInstanceInformation;
+import org.apache.helix.cloud.azure.AzureCloudInstanceInformationProcessor;
+import org.apache.helix.cloud.constants.CloudProvider;
+import org.apache.helix.model.CloudConfig;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+/**
+ * Unit test for {@link AzureCloudInstanceInformationProcessor}
+ */
+public class TestAzureCloudInstanceInformationProcessor extends MockHttpClient {
+
+  @Test()
+  public void testAzureCloudInstanceInformationProcessing() throws Exception {
+    String responseFile = "AzureResponse.json";
+
+    CloudConfig.Builder cloudConfigBuilder = new CloudConfig.Builder();
+    cloudConfigBuilder.setCloudEnabled(true);
+    cloudConfigBuilder.setCloudProvider(CloudProvider.AZURE);
+    cloudConfigBuilder.setCloudID("TestID");
+    HelixCloudProperty helixCloudProperty = new HelixCloudProperty(cloudConfigBuilder.build());
+    AzureCloudInstanceInformationProcessor processor = new AzureCloudInstanceInformationProcessor(
+        helixCloudProperty, createMockHttpClient(responseFile));
+    List<String> response = processor.fetchCloudInstanceInformation();
+
+    Assert.assertEquals(response.size(), 1);
+    Assert.assertNotNull(response.get(0));
+
+    // Verify the response from mock http client
+    AzureCloudInstanceInformation azureCloudInstanceInformation =
+        processor.parseCloudInstanceInformation(response);
+    Assert.assertEquals(azureCloudInstanceInformation
+        .get(CloudInstanceInformation.CloudInstanceField.FAULT_DOMAIN.name()), "2");
+    Assert.assertEquals(
+        azureCloudInstanceInformation
+            .get(CloudInstanceInformation.CloudInstanceField.INSTANCE_SET_NAME.name()),
+        "test-helix");
+    Assert.assertEquals(
+        azureCloudInstanceInformation
+            .get(CloudInstanceInformation.CloudInstanceField.INSTANCE_NAME.name()),
+        "d2b921cc-c16c-41f7-a86d-a445eac6ec26");
+  }
+}
diff --git a/helix-core/src/test/resources/AzureResponse.json b/helix-core/src/test/resources/AzureResponse.json
new file mode 100644
index 0000000..dfe13ad
--- /dev/null
+++ b/helix-core/src/test/resources/AzureResponse.json
@@ -0,0 +1,104 @@
+{
+  "compute": {
+    "azEnvironment": "AzurePublicCloud",
+    "customData": "",
+    "location": "southcentralus",
+    "name": "test-helix_1",
+    "offer": "",
+    "osType": "Linux",
+    "placementGroupId": "81e605b2-a807-48ee-a84a-63c76a9c9543",
+    "plan": {
+      "name": "",
+      "product": "",
+      "publisher": ""
+    },
+    "platformFaultDomain": "2",
+    "platformUpdateDomain": "2",
+    "provider": "Microsoft.Compute",
+    "publicKeys": [],
+    "publisher": "",
+    "resourceGroupName": "scus-lpsazureei1-app-rg",
+    "resourceId": "/subscriptions/c9a251d8-1272-4c0f-8055-8271bbc1d677/resourceGroups/scus-lpsazureei1-app-rg/providers/Microsoft.Compute/virtualMachines/test-helix_2",
+    "sku": "",
+    "storageProfile": {
+      "dataDisks": [],
+      "imageReference": {
+        "id": "/subscriptions/7dd5a659-67c4-441c-ac0b-d48b7a029668/resourceGroups/scus-infra-app-rg/providers/Microsoft.Compute/galleries/pieimagerepo/images/FastCOP4/versions/190924.1.1",
+        "offer": "",
+        "publisher": "",
+        "sku": "",
+        "version": ""
+      },
+      "osDisk": {
+        "caching": "ReadWrite",
+        "createOption": "FromImage",
+        "diskSizeGB": "32",
+        "encryptionSettings": {
+          "enabled": "false"
+        },
+        "image": {
+          "uri": ""
+        },
+        "managedDisk": {
+          "id": "/subscriptions/c9a251d8-1272-4c0f-8055-8271bbc1d677/resourceGroups/scus-lpsazureei1-app-rg/providers/Microsoft.Compute/disks/test-helix_test-helix_2_OsDisk_1_124c3534b8e848e296ec22b24d44c027",
+          "storageAccountType": "Standard_LRS"
+        },
+        "name": "test-helix_test-helix_2_OsDisk_1_124c3534b8e848e296ec22b24d44c027",
+        "osType": "Linux",
+        "vhd": {
+          "uri": ""
+        },
+        "writeAcceleratorEnabled": "false"
+      }
+    },
+    "subscriptionId": "c9a251d8-1272-4c0f-8055-8271bbc1d677",
+    "tags": "automation:terraform;environment:dev;module:lid-vmss;moduleVersion:0.0.1",
+    "tagsList": [
+      {
+        "name": "automation",
+        "value": "terraform"
+      },
+      {
+        "name": "environment",
+        "value": "dev"
+      },
+      {
+        "name": "module",
+        "value": "lid-vmss"
+      },
+      {
+        "name": "moduleVersion",
+        "value": "0.0.1"
+      }
+    ],
+    "version": "",
+    "vmId": "d2b921cc-c16c-41f7-a86d-a445eac6ec26",
+    "vmScaleSetName": "test-helix",
+    "vmSize": "Standard_D16s_v3",
+    "zone": ""
+  },
+  "network": {
+    "interface": [
+      {
+        "ipv4": {
+          "ipAddress": [
+            {
+              "privateIpAddress": "10.3.64.12",
+              "publicIpAddress": ""
+            }
+          ],
+          "subnet": [
+            {
+              "address": "10.3.64.0",
+              "prefix": "19"
+            }
+          ]
+        },
+        "ipv6": {
+          "ipAddress": []
+        },
+        "macAddress": "000D3A769010"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/helix-rest/pom.xml b/helix-rest/pom.xml
index e3421d5..d5b3ee5 100644
--- a/helix-rest/pom.xml
+++ b/helix-rest/pom.xml
@@ -69,11 +69,6 @@ under the License.
       <version>3.8.1</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.httpcomponents</groupId>
-      <artifactId>httpclient</artifactId>
-      <version>4.5.8</version>
-    </dependency>
-    <dependency>
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-server</artifactId>
       <version>9.1.0.RC0</version>