NIFI-2661 - Abstract the GeoEnrichIP processor NIFI-2661 - Add accuracy radius to GeoEnrichIP NIFI-2661 - Introduce ISPEnrichIP Processor

Signed-off-by: Matt Burgess <mattyb149@apache.org>

NIFI-2661 - Addresses PR comments

Signed-off-by: Matt Burgess <mattyb149@apache.org>

NIFI-2661 - Addresses PR comments

Signed-off-by: Matt Burgess <mattyb149@apache.org>

NIFI-2661: Fixed typos and reversed logging params

This closes #1650
This commit is contained in:
Andre F de Miranda 2017-04-05 12:05:51 +10:00 committed by Matt Burgess
parent 4303e4742a
commit 5cacc52cfc
5 changed files with 597 additions and 93 deletions

View File

@ -0,0 +1,120 @@
/*
* 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.nifi.processors;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.expression.AttributeExpression;
import org.apache.nifi.processor.AbstractProcessor;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessorInitializationContext;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.maxmind.DatabaseReader;
import org.apache.nifi.util.StopWatch;
public abstract class AbstractEnrichIP extends AbstractProcessor {
public static final PropertyDescriptor GEO_DATABASE_FILE = new PropertyDescriptor.Builder()
// Name has been left untouched so that we don't cause a breaking change
// but ideally this should be renamed to MaxMind Database File or something similar
.name("Geo Database File")
.displayName("MaxMind Database File")
.description("Path to Maxmind IP Enrichment Database File")
.required(true)
.addValidator(StandardValidators.FILE_EXISTS_VALIDATOR)
.build();
public static final PropertyDescriptor IP_ADDRESS_ATTRIBUTE = new PropertyDescriptor.Builder()
.name("IP Address Attribute")
.displayName("IP Address Attribute")
.required(true)
.description("The name of an attribute whose value is a dotted decimal IP address for which enrichment should occur")
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING))
.build();
public static final Relationship REL_FOUND = new Relationship.Builder()
.name("found")
.description("Where to route flow files after successfully enriching attributes with data provided by database")
.build();
public static final Relationship REL_NOT_FOUND = new Relationship.Builder()
.name("not found")
.description("Where to route flow files after unsuccessfully enriching attributes because no data was found")
.build();
private Set<Relationship> relationships;
private List<PropertyDescriptor> propertyDescriptors;
final AtomicReference<DatabaseReader> databaseReaderRef = new AtomicReference<>(null);
@Override
public Set<Relationship> getRelationships() {
return relationships;
}
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return propertyDescriptors;
}
@OnScheduled
public void onScheduled(final ProcessContext context) throws IOException {
final String dbFileString = context.getProperty(GEO_DATABASE_FILE).getValue();
final File dbFile = new File(dbFileString);
final StopWatch stopWatch = new StopWatch(true);
final DatabaseReader reader = new DatabaseReader.Builder(dbFile).build();
stopWatch.stop();
getLogger().info("Completed loading of Maxmind Database. Elapsed time was {} milliseconds.", new Object[]{stopWatch.getDuration(TimeUnit.MILLISECONDS)});
databaseReaderRef.set(reader);
}
@OnStopped
public void closeReader() throws IOException {
final DatabaseReader reader = databaseReaderRef.get();
if (reader != null) {
reader.close();
}
}
@Override
protected void init(final ProcessorInitializationContext context) {
final Set<Relationship> rels = new HashSet<>();
rels.add(REL_FOUND);
rels.add(REL_NOT_FOUND);
this.relationships = Collections.unmodifiableSet(rels);
final List<PropertyDescriptor> props = new ArrayList<>();
props.add(GEO_DATABASE_FILE);
props.add(IP_ADDRESS_ATTRIBUTE);
this.propertyDescriptors = Collections.unmodifiableList(props);
}
}

View File

@ -16,18 +16,11 @@
*/ */
package org.apache.nifi.processors; package org.apache.nifi.processors;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.EventDriven;
@ -39,18 +32,10 @@ import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.behavior.WritesAttributes; import org.apache.nifi.annotation.behavior.WritesAttributes;
import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.expression.AttributeExpression;
import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.AbstractProcessor;
import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.ProcessorInitializationContext;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.maxmind.DatabaseReader; import org.apache.nifi.processors.maxmind.DatabaseReader;
import org.apache.nifi.util.StopWatch; import org.apache.nifi.util.StopWatch;
@ -70,6 +55,7 @@ import com.maxmind.geoip2.record.Subdivision;
@WritesAttributes({ @WritesAttributes({
@WritesAttribute(attribute = "X.geo.lookup.micros", description = "The number of microseconds that the geo lookup took"), @WritesAttribute(attribute = "X.geo.lookup.micros", description = "The number of microseconds that the geo lookup took"),
@WritesAttribute(attribute = "X.geo.city", description = "The city identified for the IP address"), @WritesAttribute(attribute = "X.geo.city", description = "The city identified for the IP address"),
@WritesAttribute(attribute = "X.geo.accuracy", description = "The accuracy radius if provided by the database (in Kilometers)"),
@WritesAttribute(attribute = "X.geo.latitude", description = "The latitude identified for this IP address"), @WritesAttribute(attribute = "X.geo.latitude", description = "The latitude identified for this IP address"),
@WritesAttribute(attribute = "X.geo.longitude", description = "The longitude identified for this IP address"), @WritesAttribute(attribute = "X.geo.longitude", description = "The longitude identified for this IP address"),
@WritesAttribute(attribute = "X.geo.subdivision.N", @WritesAttribute(attribute = "X.geo.subdivision.N",
@ -78,81 +64,7 @@ import com.maxmind.geoip2.record.Subdivision;
@WritesAttribute(attribute = "X.geo.country", description = "The country identified for this IP address"), @WritesAttribute(attribute = "X.geo.country", description = "The country identified for this IP address"),
@WritesAttribute(attribute = "X.geo.country.isocode", description = "The ISO Code for the country identified"), @WritesAttribute(attribute = "X.geo.country.isocode", description = "The ISO Code for the country identified"),
@WritesAttribute(attribute = "X.geo.postalcode", description = "The postal code for the country identified"),}) @WritesAttribute(attribute = "X.geo.postalcode", description = "The postal code for the country identified"),})
public class GeoEnrichIP extends AbstractProcessor { public class GeoEnrichIP extends AbstractEnrichIP {
public static final PropertyDescriptor GEO_DATABASE_FILE = new PropertyDescriptor.Builder()
.name("Geo Database File")
.displayName("Geo Database File")
.description("Path to Maxmind Geo Enrichment Database File")
.required(true)
.addValidator(StandardValidators.FILE_EXISTS_VALIDATOR)
.build();
public static final PropertyDescriptor IP_ADDRESS_ATTRIBUTE = new PropertyDescriptor.Builder()
.name("IP Address Attribute")
.displayName("IP Address Attribute")
.required(true)
.description("The name of an attribute whose value is a dotted decimal IP address for which enrichment should occur")
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING))
.build();
public static final Relationship REL_FOUND = new Relationship.Builder()
.name("found")
.description("Where to route flow files after successfully enriching attributes with geo data")
.build();
public static final Relationship REL_NOT_FOUND = new Relationship.Builder()
.name("not found")
.description("Where to route flow files after unsuccessfully enriching attributes because no geo data was found")
.build();
private Set<Relationship> relationships;
private List<PropertyDescriptor> propertyDescriptors;
final AtomicReference<DatabaseReader> databaseReaderRef = new AtomicReference<>(null);
@Override
public Set<Relationship> getRelationships() {
return relationships;
}
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return propertyDescriptors;
}
@OnScheduled
public void onScheduled(final ProcessContext context) throws IOException {
final String dbFileString = context.getProperty(GEO_DATABASE_FILE).getValue();
final File dbFile = new File(dbFileString);
final StopWatch stopWatch = new StopWatch(true);
final DatabaseReader reader = new DatabaseReader.Builder(dbFile).build();
stopWatch.stop();
getLogger().info("Completed loading of Maxmind Geo Database. Elapsed time was {} milliseconds.", new Object[]{stopWatch.getDuration(TimeUnit.MILLISECONDS)});
databaseReaderRef.set(reader);
}
@OnStopped
public void closeReader() throws IOException {
final DatabaseReader reader = databaseReaderRef.get();
if (reader != null) {
reader.close();
}
}
@Override
protected void init(final ProcessorInitializationContext context) {
final Set<Relationship> rels = new HashSet<>();
rels.add(REL_FOUND);
rels.add(REL_NOT_FOUND);
this.relationships = Collections.unmodifiableSet(rels);
final List<PropertyDescriptor> props = new ArrayList<>();
props.add(GEO_DATABASE_FILE);
props.add(IP_ADDRESS_ATTRIBUTE);
this.propertyDescriptors = Collections.unmodifiableList(props);
}
@Override @Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
@ -164,11 +76,14 @@ public class GeoEnrichIP extends AbstractProcessor {
final DatabaseReader dbReader = databaseReaderRef.get(); final DatabaseReader dbReader = databaseReaderRef.get();
final String ipAttributeName = context.getProperty(IP_ADDRESS_ATTRIBUTE).evaluateAttributeExpressions(flowFile).getValue(); final String ipAttributeName = context.getProperty(IP_ADDRESS_ATTRIBUTE).evaluateAttributeExpressions(flowFile).getValue();
final String ipAttributeValue = flowFile.getAttribute(ipAttributeName); final String ipAttributeValue = flowFile.getAttribute(ipAttributeName);
if (StringUtils.isEmpty(ipAttributeName)) { //TODO need to add additional validation - should look like an IPv4 or IPv6 addr for instance
if (StringUtils.isEmpty(ipAttributeName)) {
session.transfer(flowFile, REL_NOT_FOUND); session.transfer(flowFile, REL_NOT_FOUND);
getLogger().warn("Unable to find ip address for {}", new Object[]{flowFile}); getLogger().warn("FlowFile '{}' attribute '{}' was empty. Routing to failure",
new Object[]{flowFile, IP_ADDRESS_ATTRIBUTE.getDisplayName()});
return; return;
} }
InetAddress inetAddress = null; InetAddress inetAddress = null;
CityResponse response = null; CityResponse response = null;
@ -176,14 +91,21 @@ public class GeoEnrichIP extends AbstractProcessor {
inetAddress = InetAddress.getByName(ipAttributeValue); inetAddress = InetAddress.getByName(ipAttributeValue);
} catch (final IOException ioe) { } catch (final IOException ioe) {
session.transfer(flowFile, REL_NOT_FOUND); session.transfer(flowFile, REL_NOT_FOUND);
getLogger().warn("Could not resolve {} to ip address for {}", new Object[]{ipAttributeValue, flowFile}, ioe); getLogger().warn("Could not resolve the IP for value '{}', contained within the attribute '{}' in " +
"FlowFile '{}'. This is usually caused by issue resolving the appropriate DNS record or " +
"providing the processor with an invalid IP address ",
new Object[]{ipAttributeValue, IP_ADDRESS_ATTRIBUTE.getDisplayName(), flowFile}, ioe);
return; return;
} }
final StopWatch stopWatch = new StopWatch(true); final StopWatch stopWatch = new StopWatch(true);
try { try {
response = dbReader.city(inetAddress); response = dbReader.city(inetAddress);
stopWatch.stop(); stopWatch.stop();
} catch (final IOException | GeoIp2Exception ex) { } catch (final IOException | GeoIp2Exception ex) {
// Note IOException is captured again as dbReader also makes InetAddress.getByName() calls.
// Most name or IP resolutions failure should have been triggered in the try loop above but
// environmental conditions may trigger errors during the second resolution as well.
session.transfer(flowFile, REL_NOT_FOUND); session.transfer(flowFile, REL_NOT_FOUND);
getLogger().warn("Failure while trying to find enrichment data for {} due to {}", new Object[]{flowFile, ex}, ex); getLogger().warn("Failure while trying to find enrichment data for {} due to {}", new Object[]{flowFile, ex}, ex);
return; return;
@ -208,6 +130,11 @@ public class GeoEnrichIP extends AbstractProcessor {
attrs.put(new StringBuilder(ipAttributeName).append(".geo.longitude").toString(), longitude.toString()); attrs.put(new StringBuilder(ipAttributeName).append(".geo.longitude").toString(), longitude.toString());
} }
final Integer accuracy = response.getLocation().getAccuracyRadius();
if (accuracy != null) {
attrs.put(new StringBuilder(ipAttributeName).append(".accuracy").toString(), String.valueOf(accuracy));
}
int i = 0; int i = 0;
for (final Subdivision subd : response.getSubdivisions()) { for (final Subdivision subd : response.getSubdivisions()) {
attrs.put(new StringBuilder(ipAttributeName).append(".geo.subdivision.").append(i).toString(), subd.getName()); attrs.put(new StringBuilder(ipAttributeName).append(".geo.subdivision.").append(i).toString(), subd.getName());

View File

@ -0,0 +1,142 @@
/*
* 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.nifi.processors;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.IspResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.EventDriven;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.behavior.SideEffectFree;
import org.apache.nifi.annotation.behavior.SupportsBatching;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.behavior.WritesAttributes;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processors.maxmind.DatabaseReader;
import org.apache.nifi.util.StopWatch;
import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@EventDriven
@SideEffectFree
@SupportsBatching
@Tags({"ISP", "enrich", "ip", "maxmind"})
@InputRequirement(Requirement.INPUT_REQUIRED)
@CapabilityDescription("Looks up ISP information for an IP address and adds the information to FlowFile attributes. The "
+ "ISP data is provided as a MaxMind ISP database (Note that this is NOT the same as the GeoLite database utilized" +
"by some geo enrichment tools). The attribute that contains the IP address to lookup is provided by the " +
"'IP Address Attribute' property. If the name of the attribute provided is 'X', then the the attributes added by" +
" enrichment will take the form X.isp.<fieldName>")
@WritesAttributes({
@WritesAttribute(attribute = "X.isp.lookup.micros", description = "The number of microseconds that the geo lookup took"),
@WritesAttribute(attribute = "X.isp.asn", description = "The Autonomous System Number (ASN) identified for the IP address"),
@WritesAttribute(attribute = "X.isp.asn.organization", description = "The Organization Associated with the ASN identified"),
@WritesAttribute(attribute = "X.isp.name", description = "The name of the ISP associated with the IP address provided"),
@WritesAttribute(attribute = "X.isp.organization", description = "The Organization associated with the IP address provided"),})
public class ISPEnrichIP extends AbstractEnrichIP {
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
FlowFile flowFile = session.get();
if (flowFile == null) {
return;
}
final DatabaseReader dbReader = databaseReaderRef.get();
final String ipAttributeName = context.getProperty(IP_ADDRESS_ATTRIBUTE).evaluateAttributeExpressions(flowFile).getValue();
final String ipAttributeValue = flowFile.getAttribute(ipAttributeName);
if (StringUtils.isEmpty(ipAttributeName)) {
session.transfer(flowFile, REL_NOT_FOUND);
getLogger().warn("FlowFile '{}' attribute '{}' was empty. Routing to failure",
new Object[]{flowFile, IP_ADDRESS_ATTRIBUTE.getDisplayName()});
return;
}
InetAddress inetAddress = null;
IspResponse response = null;
try {
inetAddress = InetAddress.getByName(ipAttributeValue);
} catch (final IOException ioe) {
session.transfer(flowFile, REL_NOT_FOUND);
getLogger().warn("Could not resolve the IP for value '{}', contained within the attribute '{}' in " +
"FlowFile '{}'. This is usually caused by issue resolving the appropriate DNS record or " +
"providing the processor with an invalid IP address ",
new Object[]{ipAttributeValue, IP_ADDRESS_ATTRIBUTE.getDisplayName(), flowFile}, ioe);
return;
}
final StopWatch stopWatch = new StopWatch(true);
try {
response = dbReader.isp(inetAddress);
stopWatch.stop();
} catch (final IOException | GeoIp2Exception ex) {
// Note IOException is captured again as dbReader also makes InetAddress.getByName() calls.
// Most name or IP resolutions failure should have been triggered in the try loop above but
// environmental conditions may trigger errors during the second resolution as well.
session.transfer(flowFile, REL_NOT_FOUND);
getLogger().warn("Failure while trying to find enrichment data for {} due to {}", new Object[]{flowFile, ex}, ex);
return;
}
if (response == null) {
session.transfer(flowFile, REL_NOT_FOUND);
return;
}
final Map<String, String> attrs = new HashMap<>();
attrs.put(new StringBuilder(ipAttributeName).append(".isp.lookup.micros").toString(), String.valueOf(stopWatch.getDuration(TimeUnit.MICROSECONDS)));
// During test I observed behavior where null values in ASN data could trigger NPEs. Instead of relying on the
// underlying database to be free from Nulls wrapping ensure equality to null without assigning a variable
// seem like good option to "final int asn ..." as with the other returned data.
if (!(response.getAutonomousSystemNumber() == null)) {
attrs.put(new StringBuilder(ipAttributeName).append(".isp.asn").toString(), String.valueOf(response.getAutonomousSystemNumber()));
}
final String asnOrg = response.getAutonomousSystemOrganization();
if (asnOrg != null) {
attrs.put(new StringBuilder(ipAttributeName).append(".isp.asn.organization").toString(), asnOrg);
}
final String ispName = response.getIsp();
if (ispName != null) {
attrs.put(new StringBuilder(ipAttributeName).append(".isp.name").toString(), ispName);
}
final String organisation = response.getOrganization();
if (organisation != null) {
attrs.put(new StringBuilder(ipAttributeName).append(".isp.organization").toString(), organisation);
}
flowFile = session.putAllAttributes(flowFile, attrs);
session.transfer(flowFile, REL_FOUND);
}
}

View File

@ -14,5 +14,6 @@
# limitations under the License. # limitations under the License.
org.apache.nifi.processors.GeoEnrichIP org.apache.nifi.processors.GeoEnrichIP
org.apache.nifi.processors.ISPEnrichIP
org.apache.nifi.processors.enrich.QueryDNS org.apache.nifi.processors.enrich.QueryDNS
org.apache.nifi.processors.enrich.QueryWhois org.apache.nifi.processors.enrich.QueryWhois

View File

@ -0,0 +1,314 @@
/*
* 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.nifi.processors;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.IspResponse;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processors.maxmind.DatabaseReader;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ISPEnrichIP.class})
@SuppressWarnings("WeakerAccess")
public class TestISPEnrichIP {
DatabaseReader databaseReader;
ISPEnrichIP ispEnrichIP;
TestRunner testRunner;
@Before
public void setUp() throws Exception {
mockStatic(InetAddress.class);
databaseReader = mock(DatabaseReader.class);
ispEnrichIP = new TestableIspEnrichIP();
testRunner = TestRunners.newTestRunner(ispEnrichIP);
}
@Test
public void verifyNonExistentIpFlowsToNotFoundRelationship() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
testRunner.enqueue(new byte[0], Collections.emptyMap());
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(1, notFound.size());
assertEquals(0, found.size());
verify(databaseReader).isp(InetAddress.getByName(null));
}
@Test
public void successfulMaxMindResponseShouldFlowToFoundRelationship() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
final IspResponse ispResponse = getIspResponse();
when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenReturn(ispResponse);
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip", "1.2.3.4");
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(0, notFound.size());
assertEquals(1, found.size());
FlowFile finishedFound = found.get(0);
assertNotNull(finishedFound.getAttribute("ip.isp.lookup.micros"));
assertEquals("Apache NiFi - Test ISP", finishedFound.getAttribute("ip.isp.name"));
assertEquals("Apache NiFi - Test Organization", finishedFound.getAttribute("ip.isp.organization"));
assertEquals("1337", finishedFound.getAttribute("ip.isp.asn"));
assertEquals("Apache NiFi - Test Chocolate", finishedFound.getAttribute("ip.isp.asn.organization"));
}
@Test
public void successfulMaxMindResponseShouldFlowToFoundRelationshipWhenAsnIsNotSet() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
final IspResponse ispResponse = getIspResponseWithoutASNDetail();
when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenReturn(ispResponse);
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip", "1.2.3.4");
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(0, notFound.size());
assertEquals(1, found.size());
FlowFile finishedFound = found.get(0);
assertNotNull(finishedFound.getAttribute("ip.isp.lookup.micros"));
assertNotNull(finishedFound.getAttribute("ip.isp.lookup.micros"));
assertEquals("Apache NiFi - Test ISP", finishedFound.getAttribute("ip.isp.name"));
assertEquals("Apache NiFi - Test Organization", finishedFound.getAttribute("ip.isp.organization"));
assertNull(finishedFound.getAttribute("ip.isp.asn"));
assertNull(finishedFound.getAttribute("ip.isp.asn.organization"));
}
@Test
public void evaluatingExpressionLanguageShouldAndFindingIpFieldWithSuccessfulLookUpShouldFlowToFoundRelationship() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "${ip.fields:substringBefore(',')}");
final IspResponse ispResponse = getIspResponse();
when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenReturn(ispResponse);
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip.fields", "ip0,ip1,ip2");
attributes.put("ip0", "1.2.3.4");
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(0, notFound.size());
assertEquals(1, found.size());
FlowFile finishedFound = found.get(0);
assertNotNull(finishedFound.getAttribute("ip0.isp.lookup.micros"));
assertEquals("Apache NiFi - Test ISP", finishedFound.getAttribute("ip0.isp.name"));
assertEquals("Apache NiFi - Test Organization", finishedFound.getAttribute("ip0.isp.organization"));
assertEquals("1337", finishedFound.getAttribute("ip0.isp.asn"));
assertEquals("Apache NiFi - Test Chocolate", finishedFound.getAttribute("ip0.isp.asn.organization"));
}
@Test
public void shouldFlowToNotFoundWhenNullResponseFromMaxMind() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenReturn(null);
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip", "1.2.3.4");
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(1, notFound.size());
assertEquals(0, found.size());
}
@SuppressWarnings("unchecked")
@Test
public void shouldFlowToNotFoundWhenIOExceptionThrownFromMaxMind() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenThrow(IOException.class);
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip", "1.2.3.4");
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(1, notFound.size());
assertEquals(0, found.size());
}
@SuppressWarnings("unchecked")
@Test
public void shouldFlowToNotFoundWhenGeoIp2ExceptionThrownFromMaxMind() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
when(databaseReader.isp(InetAddress.getByName("1.2.3.4"))).thenThrow(GeoIp2Exception.class);
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip", "1.2.3.4");
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(1, notFound.size());
assertEquals(0, found.size());
}
@SuppressWarnings("unchecked")
@Test
public void whenInetAddressThrowsUnknownHostFlowFileShouldBeSentToNotFound() throws Exception {
testRunner.setProperty(ISPEnrichIP.GEO_DATABASE_FILE, "./");
testRunner.setProperty(ISPEnrichIP.IP_ADDRESS_ATTRIBUTE, "ip");
final Map<String, String> attributes = new HashMap<>();
attributes.put("ip", "somenonexistentdomain.comm");
when(InetAddress.getByName("somenonexistentdomain.comm")).thenThrow(UnknownHostException.class);
testRunner.enqueue(new byte[0], attributes);
testRunner.run();
List<MockFlowFile> notFound = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_NOT_FOUND);
List<MockFlowFile> found = testRunner.getFlowFilesForRelationship(ISPEnrichIP.REL_FOUND);
assertEquals(1, notFound.size());
assertEquals(0, found.size());
verify(databaseReader).close();
verifyNoMoreInteractions(databaseReader);
}
private IspResponse getIspResponse() throws Exception {
// Taken from MaxMind unit tests.
final String maxMindIspResponse = "{\n" +
" \"isp\" : \"Apache NiFi - Test ISP\",\n" +
" \"organization\" : \"Apache NiFi - Test Organization\",\n" +
" \"autonomous_system_number\" : 1337,\n" +
" \"autonomous_system_organization\" : \"Apache NiFi - Test Chocolate\" \n" +
" }\n";
InjectableValues inject = new InjectableValues.Std().addValue("locales", Collections.singletonList("en"));
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return new ObjectMapper().reader(IspResponse.class).with(inject).readValue(maxMindIspResponse);
} private IspResponse getIspResponseWithoutASNDetail() throws Exception {
// Taken from MaxMind unit tests.
final String maxMindIspResponse = "{\n" +
" \"isp\" : \"Apache NiFi - Test ISP\",\n" +
" \"organization\" : \"Apache NiFi - Test Organization\",\n" +
" \"autonomous_system_number\" : null " +
" }\n";
InjectableValues inject = new InjectableValues.Std().addValue("locales", Collections.singletonList("en"));
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return new ObjectMapper().reader(IspResponse.class).with(inject).readValue(maxMindIspResponse);
}
class TestableIspEnrichIP extends ISPEnrichIP {
@OnScheduled
@Override
public void onScheduled(ProcessContext context) throws IOException {
databaseReaderRef.set(databaseReader);
}
}
}