From 6fe03cfade6832f426a238beae53e370920133fc Mon Sep 17 00:00:00 2001 From: Jeff Leung Date: Thu, 4 Jan 2024 20:39:36 -0800 Subject: [PATCH] Add project files. --- .dockerignore | 30 +++++ AS1024.GeoFeed.sln | 25 ++++ AS1024.GeoFeed/AS1024.GeoFeed.csproj | 16 +++ AS1024.GeoFeed/AS1024.GeoFeed.http | 6 + .../Controllers/GeofeedController.cs | 48 +++++++ AS1024.GeoFeed/Dockerfile | 24 ++++ .../GeoFeedBuilder/GeoFeedBuilder.cs | 36 ++++++ AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs | 20 +++ .../GeoFeedBuilder/NetBoxGeoFeedProvider.cs | 122 ++++++++++++++++++ AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs | 10 ++ AS1024.GeoFeed/Models/GeoFeed.cs | 35 +++++ AS1024.GeoFeed/Program.cs | 32 +++++ AS1024.GeoFeed/Properties/launchSettings.json | 40 ++++++ AS1024.GeoFeed/appsettings.Development.json | 8 ++ AS1024.GeoFeed/appsettings.json | 11 ++ 15 files changed, 463 insertions(+) create mode 100644 .dockerignore create mode 100644 AS1024.GeoFeed.sln create mode 100644 AS1024.GeoFeed/AS1024.GeoFeed.csproj create mode 100644 AS1024.GeoFeed/AS1024.GeoFeed.http create mode 100644 AS1024.GeoFeed/Controllers/GeofeedController.cs create mode 100644 AS1024.GeoFeed/Dockerfile create mode 100644 AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs create mode 100644 AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs create mode 100644 AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs create mode 100644 AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs create mode 100644 AS1024.GeoFeed/Models/GeoFeed.cs create mode 100644 AS1024.GeoFeed/Program.cs create mode 100644 AS1024.GeoFeed/Properties/launchSettings.json create mode 100644 AS1024.GeoFeed/appsettings.Development.json create mode 100644 AS1024.GeoFeed/appsettings.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/AS1024.GeoFeed.sln b/AS1024.GeoFeed.sln new file mode 100644 index 0000000..848634e --- /dev/null +++ b/AS1024.GeoFeed.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed", "AS1024.GeoFeed\AS1024.GeoFeed.csproj", "{6292097C-7F35-45BB-B2B0-1918DF49FE7D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6292097C-7F35-45BB-B2B0-1918DF49FE7D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {66AEF076-9E39-4263-9719-4197027D1059} + EndGlobalSection +EndGlobal diff --git a/AS1024.GeoFeed/AS1024.GeoFeed.csproj b/AS1024.GeoFeed/AS1024.GeoFeed.csproj new file mode 100644 index 0000000..3d04b4f --- /dev/null +++ b/AS1024.GeoFeed/AS1024.GeoFeed.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + true + Linux + + + + + + + + diff --git a/AS1024.GeoFeed/AS1024.GeoFeed.http b/AS1024.GeoFeed/AS1024.GeoFeed.http new file mode 100644 index 0000000..513a611 --- /dev/null +++ b/AS1024.GeoFeed/AS1024.GeoFeed.http @@ -0,0 +1,6 @@ +@AS1024.GeoFeed_HostAddress = http://localhost:5140 + +GET {{AS1024.GeoFeed_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/AS1024.GeoFeed/Controllers/GeofeedController.cs b/AS1024.GeoFeed/Controllers/GeofeedController.cs new file mode 100644 index 0000000..1dbe7b7 --- /dev/null +++ b/AS1024.GeoFeed/Controllers/GeofeedController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using AS1024.GeoFeed.Interfaces; +using AS1024.GeoFeed.GeoFeedBuilder; +using Microsoft.Extensions.Caching.Memory; +using AS1024.GeoFeed.Models; +using System.Text; + +namespace AS1024.GeoFeed.Controllers +{ + [ApiController] + [Route("[controller]")] + [Route("[controller].csv")] + + public class GeofeedController : ControllerBase + { + private readonly IGeoFeedProvider builder; + private readonly IMemoryCache memoryCache; + private const string GeoFeedCacheKey = "GeoFeedData"; + + public GeofeedController(IGeoFeedProvider builder, + IMemoryCache memoryCache) { + this.builder = builder; + this.memoryCache = memoryCache; + } + + [HttpGet] + [Route("")] + public async Task Get() + { + if (!memoryCache.TryGetValue(GeoFeedCacheKey, out List? feed)) + { + feed = await builder.GetGeoFeedData(); + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(TimeSpan.FromHours(1)); + memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions); + } + + var csvContent = feed.ToGeoFeedCsv(); // Assuming ToGeoFeedCsv() returns a string in CSV format. + var contentBytes = Encoding.UTF8.GetBytes(csvContent); + var contentType = "text/csv"; + + return new FileContentResult(contentBytes, contentType) + { + FileDownloadName = "geofeed.csv" + }; + } + } +} diff --git a/AS1024.GeoFeed/Dockerfile b/AS1024.GeoFeed/Dockerfile new file mode 100644 index 0000000..04fbf23 --- /dev/null +++ b/AS1024.GeoFeed/Dockerfile @@ -0,0 +1,24 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["AS1024.GeoFeed/AS1024.GeoFeed.csproj", "AS1024.GeoFeed/"] +RUN dotnet restore "./AS1024.GeoFeed/./AS1024.GeoFeed.csproj" +COPY . . +WORKDIR "/src/AS1024.GeoFeed" +RUN dotnet build "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./AS1024.GeoFeed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "AS1024.GeoFeed.dll"] \ No newline at end of file diff --git a/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs new file mode 100644 index 0000000..53f0fc5 --- /dev/null +++ b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedBuilder.cs @@ -0,0 +1,36 @@ + +using AS1024.GeoFeed.Interfaces; +using Microsoft.Extensions.Caching.Memory; + +namespace AS1024.GeoFeed.GeoFeedBuilder +{ + public class PreLoadGeoFeed : IHostedService + { + private readonly ILogger logger; + private readonly IGeoFeedProvider provider; + private readonly IMemoryCache memoryCache; + private const string GeoFeedCacheKey = "GeoFeedData"; + + public PreLoadGeoFeed(ILogger logger, + IGeoFeedProvider provider, + IMemoryCache memoryCache) + { + this.logger = logger; + this.provider = provider; + this.memoryCache = memoryCache; + } + + async Task IHostedService.StartAsync(CancellationToken cancellationToken) + { + logger.LogInformation("Preloading GeoFeed data in memory..."); + var feed = await provider.GetGeoFeedData(); + var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromHours(1)); + memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions); + } + + Task IHostedService.StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs new file mode 100644 index 0000000..c76634b --- /dev/null +++ b/AS1024.GeoFeed/GeoFeedBuilder/GeoFeedTools.cs @@ -0,0 +1,20 @@ +using AS1024.GeoFeed.Models; +using System.Text; + +namespace AS1024.GeoFeed.GeoFeedBuilder +{ + public static class GeoFeedTools + { + public static string ToGeoFeedCsv(this List geoFeeds) + { + StringBuilder csvContent = new(); + + foreach (var feed in geoFeeds) + { + csvContent.AppendLine($"{feed.Prefix},{feed.GeolocCountry},{feed.GeolocRegion},{feed.GeolocCity}"); + } + + return csvContent.ToString(); + } + } +} \ No newline at end of file diff --git a/AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs b/AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs new file mode 100644 index 0000000..57aae4e --- /dev/null +++ b/AS1024.GeoFeed/GeoFeedBuilder/NetBoxGeoFeedProvider.cs @@ -0,0 +1,122 @@ +using AS1024.GeoFeed.Interfaces; +using AS1024.GeoFeed.Models; +using Newtonsoft.Json; +using System.Net.Http.Headers; +using System.Net.Sockets; +using System.Web; + +namespace AS1024.GeoFeed.GeoFeedBuilder +{ + public class NetBoxGeoFeedProvider : IGeoFeedProvider + { + private readonly IConfiguration configuration; + private readonly ILogger logger; + private readonly IList addressFamilies = new List() + { + AddressFamily.InterNetwork, + AddressFamily.InterNetworkV6 + }; + private readonly IHttpClientFactory httpClientFactory; + + string IGeoFeedProvider.GeoFeedProviderName => "netbox"; + + public NetBoxGeoFeedProvider(IConfiguration configuration, + ILogger logger, + IHttpClientFactory httpClientFactory) + { + this.configuration = configuration; + this.logger = logger; + this.httpClientFactory = httpClientFactory; + } + + public async Task> GetGeoFeedData() + { + var geoFeed = new List(); + using var client = httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + foreach (var family in addressFamilies) + { + Uri uri = BuildNetBoxURI(family); + NetboxData? jsonData = null; + + while (true) + { + logger.LogDebug($"Making request to {uri}..."); + + using var result = await client.GetAsync(uri); + if (result.IsSuccessStatusCode) + { + var stringResult = await result.Content.ReadAsStringAsync(); + jsonData = JsonConvert.DeserializeObject(stringResult); + + if (jsonData?.Results == null || jsonData.Results.Count == 0) + { + break; + } + + foreach (var data in jsonData.Results) + { + try + { + geoFeed.Add(new IPGeoFeed + { + Prefix = data.Prefix, + GeolocCity = data.CustomFields.GeolocCity, + GeolocRegion = data.CustomFields.GeolocRegion, + GeolocCountry = data.CustomFields.GeolocCountry, + GeolocHasLocation = data.CustomFields.GeolocHasLocation + }); + } + catch (Exception ex) + { + logger.LogError(ex.ToString()); + } + } + + if (!string.IsNullOrEmpty(jsonData.Next)) + { + uri = new Uri(jsonData.Next); + continue; + } + } + + break; + } + } + + return geoFeed; + } + + protected Uri BuildNetBoxURI(AddressFamily family) + { + var queryParameters = HttpUtility.ParseQueryString(string.Empty); + queryParameters["cf_geoloc_has_location"] = "true"; + queryParameters["limit"] = "5"; + + switch (family) + { + case AddressFamily.InterNetwork: + queryParameters["mask_length__lte"] = "24"; + queryParameters["family"] = "4"; + break; + + case AddressFamily.InterNetworkV6: + queryParameters["mask_length__lte"] = "48"; + queryParameters["family"] = "6"; + break; + } + + var endUrl = new UriBuilder + { + Path = "api/ipam/prefixes/", + Query = queryParameters.ToString(), + Host = configuration["NetBoxHost"], + Scheme = "https" + }; + + return endUrl.Uri; + } + } +} diff --git a/AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs b/AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs new file mode 100644 index 0000000..f547e95 --- /dev/null +++ b/AS1024.GeoFeed/Interfaces/GeoFeedProvider.cs @@ -0,0 +1,10 @@ +using AS1024.GeoFeed.Models; + +namespace AS1024.GeoFeed.Interfaces +{ + public interface IGeoFeedProvider + { + public string GeoFeedProviderName { get; } + public Task> GetGeoFeedData(); + } +} diff --git a/AS1024.GeoFeed/Models/GeoFeed.cs b/AS1024.GeoFeed/Models/GeoFeed.cs new file mode 100644 index 0000000..ee54101 --- /dev/null +++ b/AS1024.GeoFeed/Models/GeoFeed.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; + +namespace AS1024.GeoFeed.Models +{ + public class NetboxData + { + public List? Results { get; set; } + public string? Next { get; set; } + public string? Previous { get; set; } + } + + public class Result + { + public string? Prefix { get; set; } + [JsonProperty("custom_fields")] + public CustomFields? CustomFields { get; set; } + } + + public class CustomFields + { + [JsonProperty("geoloc_city")] + public string? GeolocCity { get; set; } + [JsonProperty("geoloc_country")] + public string? GeolocCountry { get; set; } + [JsonProperty("geoloc_has_location")] + public bool? GeolocHasLocation { get; set; } + [JsonProperty("geoloc_region")] + public string? GeolocRegion { get; set; } + } + + + public class IPGeoFeed : CustomFields { + public string? Prefix { get; set; } + } +} diff --git a/AS1024.GeoFeed/Program.cs b/AS1024.GeoFeed/Program.cs new file mode 100644 index 0000000..c61fac7 --- /dev/null +++ b/AS1024.GeoFeed/Program.cs @@ -0,0 +1,32 @@ +using AS1024.GeoFeed.GeoFeedBuilder; +using AS1024.GeoFeed.Interfaces; + +namespace AS1024.GeoFeed +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddHostedService(); + builder.Services.AddTransient(); + builder.Services.AddHttpClient(); + builder.Services.AddMemoryCache(); + // Add services to the container. + + builder.Services.AddControllers(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + + app.UseAuthorization(); + + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/AS1024.GeoFeed/Properties/launchSettings.json b/AS1024.GeoFeed/Properties/launchSettings.json new file mode 100644 index 0000000..b659ae9 --- /dev/null +++ b/AS1024.GeoFeed/Properties/launchSettings.json @@ -0,0 +1,40 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "geofeed", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5140" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "geofeed", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:30039", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/AS1024.GeoFeed/appsettings.Development.json b/AS1024.GeoFeed/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/AS1024.GeoFeed/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/AS1024.GeoFeed/appsettings.json b/AS1024.GeoFeed/appsettings.json new file mode 100644 index 0000000..fbff1fd --- /dev/null +++ b/AS1024.GeoFeed/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "APIKey": "", + "NetBoxHost": "" +}