This is an automated email from the ASF dual-hosted git repository. jiajunwang pushed a commit to branch helix-0.9.x in repository https://gitbox.apache.org/repos/asf/helix.git
commit da32e4ee26e764063af24b2b86be182123778b9f Author: zhangmeng916 <[email protected]> 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 8288d8f..95aefe3 100644 --- a/helix-core/pom.xml +++ b/helix-core/pom.xml @@ -159,6 +159,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 44550b7..76db93c 100644 --- a/helix-rest/pom.xml +++ b/helix-rest/pom.xml @@ -67,11 +67,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>
