Initial commit
This commit is contained in:
Родитель
d88de18fb5
Коммит
ab8ee74240
|
@ -0,0 +1,47 @@
|
|||
*.DS_Store
|
||||
[Tt]humbs.db
|
||||
|
||||
#Visual Studio Files
|
||||
[Oo]bj
|
||||
[Bb]in
|
||||
[Dd]ebug
|
||||
[Bb]uild/
|
||||
*.user
|
||||
*.suo
|
||||
*.exe
|
||||
*.pdb
|
||||
*.aps
|
||||
*_i.c
|
||||
*_p.c
|
||||
*.ncb
|
||||
*.tlb
|
||||
*.tlh
|
||||
*.[Cc]ache
|
||||
*.bak
|
||||
*.ncb
|
||||
*.ilk
|
||||
*.log
|
||||
*.lib
|
||||
*.sbr
|
||||
*.sdf
|
||||
ipch/
|
||||
*.dbmdl
|
||||
*.csproj.user
|
||||
*.cache
|
||||
*.swp
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
*.bak.*
|
||||
*.bak
|
||||
.vs/
|
||||
|
||||
#Tools
|
||||
_ReSharper.*
|
||||
_ReSharper*/
|
||||
*.resharper
|
||||
*.resharper.user
|
||||
[Nn][Dd]epend[Oo]ut*/
|
||||
Ankh.NoLoad
|
||||
|
||||
#Other
|
||||
.svn
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29911.98
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceBridge", "DeviceBridge\DeviceBridge.csproj", "{0956E364-75AF-4C03-A96C-DCA4D6058FA3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceBridgeTests", "DeviceBridgeTests\DeviceBridgeTests.csproj", "{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8C67BD65-1F81-4180-91B4-691483E7C916}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
namespace DeviceBridge.Common.Authentication
|
||||
{
|
||||
public static class SchemesNamesConst
|
||||
{
|
||||
public const string TokenAuthenticationDefaultScheme = "TokenAuthenticationScheme";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Providers;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Common.Authentication
|
||||
{
|
||||
public class TokenAuthenticationHandler : AuthenticationHandler<TokenAuthenticationOptions>
|
||||
{
|
||||
private ISecretsProvider secretsProvider;
|
||||
private Logger logger;
|
||||
|
||||
public TokenAuthenticationHandler(IOptionsMonitor<TokenAuthenticationOptions> options, ILoggerFactory loggerFactory, NLog.Logger logger, UrlEncoder encoder, ISystemClock clock, IServiceProvider serviceProvider, ISecretsProvider secretsProvider)
|
||||
: base(options, loggerFactory, encoder, clock)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
this.secretsProvider = secretsProvider;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public IServiceProvider ServiceProvider { get; set; }
|
||||
|
||||
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var tmpLogger = this.logger.WithProperty("cv", Utils.GuidFromString(Request.HttpContext.TraceIdentifier));
|
||||
|
||||
tmpLogger.Info("Starting api key authentication.");
|
||||
|
||||
var masterApiKey = await this.secretsProvider.GetApiKey(this.logger);
|
||||
var headers = Request.Headers;
|
||||
var apiKey = headers["x-api-key"];
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
tmpLogger.Info("Api key is null");
|
||||
return AuthenticateResult.Fail("Api key is null");
|
||||
}
|
||||
|
||||
bool isValidToken = masterApiKey.Equals(apiKey); // check token here
|
||||
|
||||
if (!isValidToken)
|
||||
{
|
||||
tmpLogger.Info($"Apikey authentication failed.");
|
||||
return AuthenticateResult.Fail($"Apikey authentication failed.");
|
||||
}
|
||||
|
||||
var claims = new[] { new Claim("apiKey", apiKey) };
|
||||
var identity = new ClaimsIdentity(claims, nameof(TokenAuthenticationHandler));
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), this.Scheme.Name);
|
||||
tmpLogger.Info("Successfully authenticated using api key.");
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace DeviceBridge.Common.Authentication
|
||||
{
|
||||
public class TokenAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Common.Exceptions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace DeviceBridge.Common
|
||||
{
|
||||
internal class ExceptionHandlingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next)
|
||||
{
|
||||
this._next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (BridgeException e)
|
||||
{
|
||||
context.Response.StatusCode = e.StatusCode;
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public abstract class BridgeException : Exception
|
||||
{
|
||||
public BridgeException(string message, int statusCode)
|
||||
: base(message)
|
||||
{
|
||||
this.StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public BridgeException(string message, Exception inner, int statusCode)
|
||||
: base(message, inner)
|
||||
{
|
||||
this.StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public int StatusCode { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class DeviceConnectionNotFoundException : BridgeException
|
||||
{
|
||||
public DeviceConnectionNotFoundException(string deviceId)
|
||||
: base($"Connection for device {deviceId} not found", StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class DeviceSdkTimeoutException : BridgeException
|
||||
{
|
||||
public DeviceSdkTimeoutException(string deviceId)
|
||||
: base($"The device SDK timed out while executing the requested operation for device {deviceId}", StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class DpsRegistrationFailedWithUnknownStatusException : BridgeException
|
||||
{
|
||||
public DpsRegistrationFailedWithUnknownStatusException(string deviceId, string status, string substatus, int? errorCode, string errorMessage)
|
||||
: base($"Failed to perform DPS registration for device {deviceId}: {status} {substatus} {errorCode} {errorMessage}", StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class EncryptionException : BridgeException
|
||||
{
|
||||
public EncryptionException()
|
||||
: base("Error when trying to run encryption or decryption.", StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class StorageSetupIncompleteException : BridgeException
|
||||
{
|
||||
public StorageSetupIncompleteException(Exception inner)
|
||||
: base("Storage setup incomplete: missing tables or stored procedures", inner, StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class UnknownDeviceSubscriptionTypeException : BridgeException
|
||||
{
|
||||
public UnknownDeviceSubscriptionTypeException(string type)
|
||||
: base($"Unknown device subscription type {type}", StatusCodes.Status400BadRequest)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DeviceBridge.Common.Exceptions
|
||||
{
|
||||
public class UnknownStorageException : BridgeException
|
||||
{
|
||||
public UnknownStorageException(Exception inner)
|
||||
: base("Unknown storage exception", inner, StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace DeviceBridge.Common
|
||||
{
|
||||
internal class RequestLoggingMiddleware
|
||||
{
|
||||
private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public RequestLoggingMiddleware(RequestDelegate next)
|
||||
{
|
||||
this._next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
var request = await FormatRequest(context.Request);
|
||||
var logger = _logger.WithProperty("cv", Utils.GuidFromString(context.TraceIdentifier));
|
||||
logger.SetProperty("path", context.Request.Path.Value);
|
||||
|
||||
var regexResult = Regex.Match(context.Request.Path, @"(?<=devices\/).*?(?=\/)");
|
||||
if (regexResult.Success)
|
||||
{
|
||||
var deviceId = regexResult.Groups[0].Value;
|
||||
logger.SetProperty("deviceId", deviceId);
|
||||
}
|
||||
|
||||
logger.Info(request);
|
||||
|
||||
var originalBodyStream = context.Response.Body;
|
||||
using (var responseBody = new MemoryStream())
|
||||
{
|
||||
context.Response.Body = responseBody;
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var tmpResponse = FormatResponse(context.Response);
|
||||
logger.Error(e, tmpResponse);
|
||||
await responseBody.CopyToAsync(originalBodyStream);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = FormatResponse(context.Response);
|
||||
logger.Info(response);
|
||||
await responseBody.CopyToAsync(originalBodyStream);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> FormatRequest(HttpRequest request)
|
||||
{
|
||||
request.EnableBuffering();
|
||||
|
||||
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
|
||||
await request.Body.ReadAsync(buffer, 0, buffer.Length);
|
||||
var requestBody = Encoding.UTF8.GetString(buffer);
|
||||
request.Body.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var builder = new StringBuilder(Environment.NewLine);
|
||||
builder.AppendLine("{ headers: {");
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
var redactedHeaderValue = RedactHeaderValue(header);
|
||||
builder.AppendLine($"{header.Key}:{redactedHeaderValue},");
|
||||
}
|
||||
|
||||
builder.AppendLine($"}},body:{requestBody}}}");
|
||||
|
||||
return builder.ToString().Replace("\r\n", string.Empty);
|
||||
}
|
||||
|
||||
private string RedactHeaderValue(System.Collections.Generic.KeyValuePair<string, StringValues> header)
|
||||
{
|
||||
if (header.Key.Equals("x-api-key"))
|
||||
{
|
||||
return "redacted";
|
||||
}
|
||||
|
||||
return header.Value;
|
||||
}
|
||||
|
||||
private string FormatResponse(HttpResponse response)
|
||||
{
|
||||
response.Body.Seek(0, SeekOrigin.Begin);
|
||||
string responseBody = new StreamReader(response.Body).ReadToEnd();
|
||||
response.Body.Seek(0, SeekOrigin.Begin);
|
||||
return $"Response: {response.StatusCode}, {responseBody}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using DeviceBridge.Providers;
|
||||
using NLog;
|
||||
|
||||
public static class Utils
|
||||
{
|
||||
private static MD5 hasher = MD5.Create();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a GUID hashed from an input string.
|
||||
/// </summary>
|
||||
/// <param name="input">Input to generate the GUID from.</param>
|
||||
/// <returns>GUID hashed from input.</returns>
|
||||
public static Guid GuidFromString(string input)
|
||||
{
|
||||
return new Guid(hasher.ComputeHash(Encoding.Default.GetBytes(input)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the sql connection string.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="secretsProvider">Secrets provider for retrieving credentials.</param>
|
||||
/// <returns>The sql connection string.</returns>
|
||||
public static string GetSqlConnectionString(Logger logger, SecretsProvider secretsProvider)
|
||||
{
|
||||
var sqlServerName = secretsProvider.GetSqlServerAsync(logger).Result;
|
||||
var sqlDatabaseName = secretsProvider.GetSqlDatabaseAsync(logger).Result;
|
||||
var sqlUsername = secretsProvider.GetSqlUsernameAsync(logger).Result;
|
||||
var sqlPassword = secretsProvider.GetSqlPasswordAsync(logger).Result;
|
||||
sqlPassword = sqlPassword.Replace("'", "''");
|
||||
return $"Server=tcp:{sqlServerName},1433;Initial Catalog={sqlDatabaseName};Persist Security Info=False;User ID={sqlUsername};Password='{sqlPassword}';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
// TODO: add proper correlation middleware (that preserves the correlationId)
|
||||
[Produces("application/json")]
|
||||
public partial class BaseController : Controller
|
||||
{
|
||||
public BaseController(Logger baseLogger)
|
||||
{
|
||||
this.Logger = baseLogger.WithProperty("cv", Guid.NewGuid()); // Create a new logger instance to be used in the controller scope.
|
||||
}
|
||||
|
||||
protected Logger Logger { get; set; }
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext filterContext)
|
||||
{
|
||||
// Configure logger
|
||||
Logger.SetProperty("path", filterContext.HttpContext.Request.Path.Value);
|
||||
Logger.SetProperty("cv", Utils.GuidFromString(filterContext.HttpContext.TraceIdentifier));
|
||||
base.OnActionExecuting(filterContext);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class ConnectionStatusController : BaseController
|
||||
{
|
||||
private readonly SubscriptionService _subscriptionService;
|
||||
private readonly ConnectionManager _connectionManager;
|
||||
|
||||
public ConnectionStatusController(Logger logger, SubscriptionService subscriptionService, ConnectionManager connectionManager)
|
||||
: base(logger)
|
||||
{
|
||||
_subscriptionService = subscriptionService;
|
||||
_connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets that latest connection status for a device.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
/// </remarks>
|
||||
/// <response code="200">The latest connection status and reason.</response>
|
||||
/// <response code="404">If the connection status is not known (i.e., the device hasn't attempted to connect).</response>
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[NotFoundResultFilter]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<DeviceStatusResponseBody> GetCurrentConnectionStatus(string deviceId)
|
||||
{
|
||||
var deviceStatus = _connectionManager.GetDeviceStatus(deviceId);
|
||||
|
||||
return (deviceStatus != null) ? new DeviceStatusResponseBody()
|
||||
{
|
||||
Status = deviceStatus?.status.ToString(),
|
||||
Reason = deviceStatus?.reason.ToString(),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection status change subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="200">The current connection status subscription.</response>
|
||||
/// <response code="404">If a subscription doesn't exist.</response>
|
||||
[HttpGet]
|
||||
[Route("sub")]
|
||||
[NotFoundResultFilter]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscription>> GetConnectionStatusSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.GetConnectionStatusSubscription(Logger, deviceId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the current connection status change subscription for a device.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the internal connection status of a device changes, the service will send an event to the desired callback URL.
|
||||
///
|
||||
/// Example event:
|
||||
/// {
|
||||
/// "eventType": "string",
|
||||
/// "deviceId": "string",
|
||||
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
/// "status": "string",
|
||||
/// "reason": "string"
|
||||
/// }
|
||||
///
|
||||
/// For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
/// </remarks>
|
||||
/// <response code="200">The created or updated connection status subscription.</response>
|
||||
[HttpPut]
|
||||
[Route("sub")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscription>> CreateOrUpdateConnectionStatusSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.CreateOrUpdateConnectionStatusSubscription(Logger, deviceId, body.CallbackUrl, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the current connection status change subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="204">Subscription deleted successfully.</response>
|
||||
[HttpDelete]
|
||||
[Route("sub")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DeleteConnectionStatusSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _subscriptionService.DeleteConnectionStatusSubscription(Logger, deviceId, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class DeviceBoundController : BaseController
|
||||
{
|
||||
private readonly SubscriptionService _subscriptionService;
|
||||
|
||||
public DeviceBoundController(Logger logger, SubscriptionService subscriptionService)
|
||||
: base(logger)
|
||||
{
|
||||
_subscriptionService = subscriptionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current C2D message subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="200">The current C2D message subscription.</response>
|
||||
/// <response code="404">If a subscription doesn't exist.</response>
|
||||
[HttpGet]
|
||||
[Route("sub")]
|
||||
[NotFoundResultFilter]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscriptionWithStatus>> GetC2DMessageSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.GetDataSubscription(Logger, deviceId, DeviceSubscriptionType.C2DMessages, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the current C2D message subscription for a device.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL.
|
||||
///
|
||||
/// Example event:
|
||||
/// {
|
||||
/// "eventType": "string",
|
||||
/// "deviceId": "string",
|
||||
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
/// "messageBody": {},
|
||||
/// "properties": {
|
||||
/// "prop1": "string",
|
||||
/// "prop2": "string",
|
||||
/// },
|
||||
/// "messageId": "string",
|
||||
/// "expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
|
||||
/// }
|
||||
///
|
||||
/// The response status code of the callback URL will determine how the service will acknowledge a message:
|
||||
/// - Response code between 200 and 299: the service will complete the message.
|
||||
/// - Response code between 400 and 499: the service will reject the message.
|
||||
/// - Any other response status: the service will abandon the message, causing IotHub to redeliver it.
|
||||
///
|
||||
/// For a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
|
||||
/// </remarks>
|
||||
/// <response code="200">The created or updated C2D message subscription.</response>
|
||||
[HttpPut]
|
||||
[Route("sub")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscriptionWithStatus>> CreateOrUpdateC2DMessageSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.CreateOrUpdateDataSubscription(Logger, deviceId, DeviceSubscriptionType.C2DMessages, body.CallbackUrl, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the current C2D message subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="204">Subscription deleted successfully.</response>
|
||||
[HttpDelete]
|
||||
[Route("sub")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DeleteC2DMessageSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _subscriptionService.DeleteDataSubscription(Logger, deviceId, DeviceSubscriptionType.C2DMessages, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using DeviceBridge.Common.Exceptions;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The response body returned if an error occours.
|
||||
/// </summary>
|
||||
public class HttpErrorBody
|
||||
{
|
||||
public HttpErrorBody(BridgeException e)
|
||||
{
|
||||
this.Message = e.Message;
|
||||
}
|
||||
|
||||
public string Message { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class MessagesController : BaseController
|
||||
{
|
||||
private readonly IBridgeService _bridgeService;
|
||||
|
||||
public MessagesController(NLog.Logger logger, IBridgeService bridgeService)
|
||||
: base(logger)
|
||||
{
|
||||
_bridgeService = bridgeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a device message to IoTHub.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example request:
|
||||
///
|
||||
/// POST /devices/{deviceId}/messages/events
|
||||
/// {
|
||||
/// "data": {
|
||||
/// "temperature": 4.8,
|
||||
/// "humidity": 31
|
||||
/// }
|
||||
/// }
|
||||
/// .
|
||||
/// </remarks>
|
||||
/// <response code="200">Message sent successfully.</response>
|
||||
[HttpPost]
|
||||
[Route("events")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> SendMessage(string deviceId, MessageBody message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Force timestamp to be interpreted as UTC.
|
||||
if (message.CreationTimeUtc is DateTime)
|
||||
{
|
||||
message.CreationTimeUtc = DateTime.SpecifyKind((DateTime)message.CreationTimeUtc, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
await _bridgeService.SendTelemetry(Logger, deviceId, message.Data, cancellationToken, message.Properties, message.ComponentName, message.CreationTimeUtc);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class MethodsController : BaseController
|
||||
{
|
||||
private readonly SubscriptionService _subscriptionService;
|
||||
|
||||
public MethodsController(Logger logger, SubscriptionService subscriptionService)
|
||||
: base(logger)
|
||||
{
|
||||
_subscriptionService = subscriptionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current direct methods subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="200">The current direct methods subscription.</response>
|
||||
/// <response code="404">If a subscription doesn't exist.</response>
|
||||
[HttpGet]
|
||||
[Route("sub")]
|
||||
[NotFoundResultFilter]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscriptionWithStatus>> GetMethodsSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.GetDataSubscription(Logger, deviceId, DeviceSubscriptionType.Methods, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the current direct methods subscription for a device.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the device receives a direct method invocation from IoTHub, the service will send an event to the desired callback URL.
|
||||
///
|
||||
/// Example event:
|
||||
/// {
|
||||
/// "eventType": "string",
|
||||
/// "deviceId": "string",
|
||||
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
/// "methodName": "string",
|
||||
/// "requestData": {}
|
||||
/// }
|
||||
///
|
||||
/// The callback may return an optional response body, which will be sent to IoTHub as the method response:
|
||||
///
|
||||
/// Example callback response:
|
||||
/// {
|
||||
/// "status": 200,
|
||||
/// "payload": {}
|
||||
/// }
|
||||
/// .
|
||||
/// </remarks>
|
||||
/// <response code="200">The created or updated C2D message subscription.</response>
|
||||
[HttpPut]
|
||||
[Route("sub")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscriptionWithStatus>> CreateOrUpdateMethodsSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.CreateOrUpdateDataSubscription(Logger, deviceId, DeviceSubscriptionType.Methods, body.CallbackUrl, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the current direct methods subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="204">Subscription deleted successfully.</response>
|
||||
[HttpDelete]
|
||||
[Route("sub")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DeleteMethodsSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _subscriptionService.DeleteDataSubscription(Logger, deviceId, DeviceSubscriptionType.Methods, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a null return value into a 404.
|
||||
/// </summary>
|
||||
public class NotFoundResultFilterAttribute : ResultFilterAttribute
|
||||
{
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (context.Result is ObjectResult objectResult && objectResult.Value == null)
|
||||
{
|
||||
context.Result = new NotFoundResult();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class RegistrationController : BaseController
|
||||
{
|
||||
private readonly ConnectionManager _connectionManager;
|
||||
|
||||
public RegistrationController(NLog.Logger logger, ConnectionManager connectionManager)
|
||||
: base(logger)
|
||||
{
|
||||
_connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs DPS registration for a device, optionally assigning it to a model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The registration result is internally cached to be used in future connections.
|
||||
/// This route is only intended for ahead-of-time registration of devices with the bridge and assignment to a specific model. To access all DPS registration features,
|
||||
/// including sending custom registration payload and getting the assigned hub, please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
|
||||
///
|
||||
/// <b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a long time to return. If this is a concern, use the DPS REST API directly, which provides
|
||||
/// support for long-running operation status lookup.
|
||||
/// </remarks>
|
||||
/// <response code="200">Registration successful.</response>
|
||||
[HttpPost]
|
||||
[Route("")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> Register(string deviceId, RegistrationBody body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _connectionManager.StandaloneDpsRegistrationAsync(Logger, deviceId, body.ModelId, cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class ResyncController : BaseController
|
||||
{
|
||||
private readonly SubscriptionService _subscriptionService;
|
||||
|
||||
public ResyncController(NLog.Logger logger, SubscriptionService subscriptionService)
|
||||
: base(logger)
|
||||
{
|
||||
_subscriptionService = subscriptionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces a full synchronization of all subscriptions for this device and attempts to restart any subscriptions in a stopped state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Internally it forces the reconnection of the device if it's in a permanent failure state, due for instance to:
|
||||
/// - Bad credentials.
|
||||
/// - Device was previously disabled in the cloud side.
|
||||
/// - Automatic retries expired (e.g., due to a long period without network connectivity).
|
||||
/// </remarks>
|
||||
/// <response code="202">Resynchronization started.</response>
|
||||
[HttpPost]
|
||||
[Route("")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
public ActionResult Resync(string deviceId)
|
||||
{
|
||||
var _ = _subscriptionService.SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId, false /* fetch latest subscriptions from DB */, true /* retry failed connection */);
|
||||
return Accepted();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Controllers
|
||||
{
|
||||
[Route("devices/{deviceId}/[controller]")]
|
||||
[ApiController]
|
||||
public class TwinController : BaseController
|
||||
{
|
||||
private readonly SubscriptionService _subscriptionService;
|
||||
private readonly BridgeService _bridgeService;
|
||||
|
||||
public TwinController(Logger logger, SubscriptionService subscriptionService, BridgeService bridgeService)
|
||||
: base(logger)
|
||||
{
|
||||
_subscriptionService = subscriptionService;
|
||||
_bridgeService = bridgeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the device twin.
|
||||
/// </summary>
|
||||
/// <response code="200">The device twin.</response>
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceTwin>> GetTwin(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = new DeviceTwin()
|
||||
{
|
||||
Twin = new JRaw((await _bridgeService.GetTwin(Logger, deviceId, cancellationToken)).ToJson()),
|
||||
};
|
||||
|
||||
return Content(JsonConvert.SerializeObject(response), "application/json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates reported properties in the device twin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example request:
|
||||
///
|
||||
/// PATCH /devices/{deviceId}/properties/reported
|
||||
/// {
|
||||
/// "patch": {
|
||||
/// "fanSpeed": 35,
|
||||
/// "serial": "ABC"
|
||||
/// }
|
||||
/// }
|
||||
/// .
|
||||
/// </remarks>
|
||||
/// <response code="204">Twin updated successfully.</response>
|
||||
[HttpPatch]
|
||||
[Route("properties/reported")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UpdateReportedProperties(string deviceId, ReportedPropertiesPatch body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _bridgeService.UpdateReportedProperties(Logger, deviceId, body.Patch, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current desired property change subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="200">The current desired property change subscription.</response>
|
||||
/// <response code="404">If a subscription doesn't exist.</response>
|
||||
[HttpGet]
|
||||
[Route("properties/desired/sub")]
|
||||
[NotFoundResultFilter]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscriptionWithStatus>> GetDesiredPropertiesSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.GetDataSubscription(Logger, deviceId, DeviceSubscriptionType.DesiredProperties, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the current desired property change subscription for a device.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the device receives a new desired property change from IoTHub, the service will send an event to the desired callback URL.
|
||||
///
|
||||
/// Example event:
|
||||
/// {
|
||||
/// "eventType": "string",
|
||||
/// "deviceId": "string",
|
||||
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
/// "desiredProperties": {
|
||||
/// "prop1": "string",
|
||||
/// "prop2": 12,
|
||||
/// "prop3": {},
|
||||
/// }
|
||||
/// }
|
||||
/// .
|
||||
/// </remarks>
|
||||
/// <response code="200">The created or updated C2D message subscription.</response>
|
||||
[HttpPut]
|
||||
[Route("properties/desired/sub")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DeviceSubscriptionWithStatus>> CreateOrUpdateDesiredPropertiesSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _subscriptionService.CreateOrUpdateDataSubscription(Logger, deviceId, DeviceSubscriptionType.DesiredProperties, body.CallbackUrl, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the current desired property change subscription for a device.
|
||||
/// </summary>
|
||||
/// <response code="204">Subscription deleted successfully.</response>
|
||||
[HttpDelete]
|
||||
[Route("properties/desired/sub")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DeleteDesiredPropertiesSubscription(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _subscriptionService.DeleteDataSubscription(Logger, deviceId, DeviceSubscriptionType.DesiredProperties, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
Make sure any documentation comments which are included in code get checked for syntax during the build, but do
|
||||
not report warnings for missing comments.
|
||||
|
||||
CS1573: Parameter 'parameter' has no matching param tag in the XML comment for 'parameter' (but other parameters do)
|
||||
CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
|
||||
-->
|
||||
<DocumentationFile>$(OutputPath)$(AssemblyName).xml</DocumentationFile>
|
||||
<NoWarn>$(NoWarn),1573,1591,1712</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="NLog.config" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="NLog.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.AzureKeyVault.HostingStartup" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices" Version="1.28.1" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.33.1" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Client" Version="1.6.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Amqp" Version="1.3.1" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Http" Version="1.2.4" />
|
||||
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.5" />
|
||||
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.5.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.4" />
|
||||
<PackageReference Include="NLog" Version="4.7.4" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Collections" Version="4.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,620 @@
|
|||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>DeviceBridge</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.GetCurrentConnectionStatus(System.String)">
|
||||
<summary>
|
||||
Gets that latest connection status for a device.
|
||||
</summary>
|
||||
<remarks>
|
||||
For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
</remarks>
|
||||
<response code="200">The latest connection status and reason.</response>
|
||||
<response code="404">If the connection status is not known (i.e., the device hasn't attempted to connect).</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.GetConnectionStatusSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Gets the current connection status change subscription for a device.
|
||||
</summary>
|
||||
<response code="200">The current connection status subscription.</response>
|
||||
<response code="404">If a subscription doesn't exist.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.CreateOrUpdateConnectionStatusSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Creates or updates the current connection status change subscription for a device.
|
||||
</summary>
|
||||
<remarks>
|
||||
When the internal connection status of a device changes, the service will send an event to the desired callback URL.
|
||||
|
||||
Example event:
|
||||
{
|
||||
"eventType": "string",
|
||||
"deviceId": "string",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"status": "string",
|
||||
"reason": "string"
|
||||
}
|
||||
|
||||
For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
</remarks>
|
||||
<response code="200">The created or updated connection status subscription.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.DeleteConnectionStatusSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Deletes the current connection status change subscription for a device.
|
||||
</summary>
|
||||
<response code="204">Subscription deleted successfully.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.DeviceBoundController.GetC2DMessageSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Gets the current C2D message subscription for a device.
|
||||
</summary>
|
||||
<response code="200">The current C2D message subscription.</response>
|
||||
<response code="404">If a subscription doesn't exist.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.DeviceBoundController.CreateOrUpdateC2DMessageSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Creates or updates the current C2D message subscription for a device.
|
||||
</summary>
|
||||
<remarks>
|
||||
When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL.
|
||||
|
||||
Example event:
|
||||
{
|
||||
"eventType": "string",
|
||||
"deviceId": "string",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"messageBody": {},
|
||||
"properties": {
|
||||
"prop1": "string",
|
||||
"prop2": "string",
|
||||
},
|
||||
"messageId": "string",
|
||||
"expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
|
||||
}
|
||||
|
||||
The response status code of the callback URL will determine how the service will acknowledge a message:
|
||||
- Response code between 200 and 299: the service will complete the message.
|
||||
- Response code between 400 and 499: the service will reject the message.
|
||||
- Any other response status: the service will abandon the message, causing IotHub to redeliver it.
|
||||
|
||||
For a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
|
||||
</remarks>
|
||||
<response code="200">The created or updated C2D message subscription.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.DeviceBoundController.DeleteC2DMessageSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Deletes the current C2D message subscription for a device.
|
||||
</summary>
|
||||
<response code="204">Subscription deleted successfully.</response>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Controllers.HttpErrorBody">
|
||||
<summary>
|
||||
The response body returned if an error occours.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.MessagesController.SendMessage(System.String,DeviceBridge.Models.MessageBody,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Sends a device message to IoTHub.
|
||||
</summary>
|
||||
<remarks>
|
||||
Example request:
|
||||
|
||||
POST /devices/{deviceId}/messages/events
|
||||
{
|
||||
"data": {
|
||||
"temperature": 4.8,
|
||||
"humidity": 31
|
||||
}
|
||||
}
|
||||
.
|
||||
</remarks>
|
||||
<response code="200">Message sent successfully.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.MethodsController.GetMethodsSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Gets the current direct methods subscription for a device.
|
||||
</summary>
|
||||
<response code="200">The current direct methods subscription.</response>
|
||||
<response code="404">If a subscription doesn't exist.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.MethodsController.CreateOrUpdateMethodsSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Creates or updates the current direct methods subscription for a device.
|
||||
</summary>
|
||||
<remarks>
|
||||
When the device receives a direct method invocation from IoTHub, the service will send an event to the desired callback URL.
|
||||
|
||||
Example event:
|
||||
{
|
||||
"eventType": "string",
|
||||
"deviceId": "string",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"methodName": "string",
|
||||
"requestData": {}
|
||||
}
|
||||
|
||||
The callback may return an optional response body, which will be sent to IoTHub as the method response:
|
||||
|
||||
Example callback response:
|
||||
{
|
||||
"status": 200,
|
||||
"payload": {}
|
||||
}
|
||||
.
|
||||
</remarks>
|
||||
<response code="200">The created or updated C2D message subscription.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.MethodsController.DeleteMethodsSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Deletes the current direct methods subscription for a device.
|
||||
</summary>
|
||||
<response code="204">Subscription deleted successfully.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.RegistrationController.Register(System.String,DeviceBridge.Models.RegistrationBody,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Performs DPS registration for a device, optionally assigning it to a model.
|
||||
</summary>
|
||||
<remarks>
|
||||
The registration result is internally cached to be used in future connections.
|
||||
This route is only intended for ahead-of-time registration of devices with the bridge and assignment to a specific model. To access all DPS registration features,
|
||||
including sending custom registration payload and getting the assigned hub, please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
|
||||
|
||||
<b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a long time to return. If this is a concern, use the DPS REST API directly, which provides
|
||||
support for long-running operation status lookup.
|
||||
</remarks>
|
||||
<response code="200">Registration successful.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.ResyncController.Resync(System.String)">
|
||||
<summary>
|
||||
Forces a full synchronization of all subscriptions for this device and attempts to restart any subscriptions in a stopped state.
|
||||
</summary>
|
||||
<remarks>
|
||||
Internally it forces the reconnection of the device if it's in a permanent failure state, due for instance to:
|
||||
- Bad credentials.
|
||||
- Device was previously disabled in the cloud side.
|
||||
- Automatic retries expired (e.g., due to a long period without network connectivity).
|
||||
</remarks>
|
||||
<response code="202">Resynchronization started.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.TwinController.GetTwin(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Gets the device twin.
|
||||
</summary>
|
||||
<response code="200">The device twin.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.TwinController.UpdateReportedProperties(System.String,DeviceBridge.Models.ReportedPropertiesPatch,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Updates reported properties in the device twin.
|
||||
</summary>
|
||||
<remarks>
|
||||
Example request:
|
||||
|
||||
PATCH /devices/{deviceId}/properties/reported
|
||||
{
|
||||
"patch": {
|
||||
"fanSpeed": 35,
|
||||
"serial": "ABC"
|
||||
}
|
||||
}
|
||||
.
|
||||
</remarks>
|
||||
<response code="204">Twin updated successfully.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.TwinController.GetDesiredPropertiesSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Gets the current desired property change subscription for a device.
|
||||
</summary>
|
||||
<response code="200">The current desired property change subscription.</response>
|
||||
<response code="404">If a subscription doesn't exist.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.TwinController.CreateOrUpdateDesiredPropertiesSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Creates or updates the current desired property change subscription for a device.
|
||||
</summary>
|
||||
<remarks>
|
||||
When the device receives a new desired property change from IoTHub, the service will send an event to the desired callback URL.
|
||||
|
||||
Example event:
|
||||
{
|
||||
"eventType": "string",
|
||||
"deviceId": "string",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"desiredProperties": {
|
||||
"prop1": "string",
|
||||
"prop2": 12,
|
||||
"prop3": {},
|
||||
}
|
||||
}
|
||||
.
|
||||
</remarks>
|
||||
<response code="200">The created or updated C2D message subscription.</response>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Controllers.TwinController.DeleteDesiredPropertiesSubscription(System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Deletes the current desired property change subscription for a device.
|
||||
</summary>
|
||||
<response code="204">Subscription deleted successfully.</response>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateUpsertDeviceSubscriptionProcedureQuery">
|
||||
<summary>
|
||||
Tries to create a device subscription. If one already exists, updates it.
|
||||
Concurrent calls to this procedure will not generate a failure.
|
||||
Outputs the creation time.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateUpsertHubCacheEntryProcedureQuery">
|
||||
<summary>
|
||||
Tries to add a hub cache entry for a device. If one already exists, updates it.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateGetHubCacheEntriesPagedProcedureQuery">
|
||||
<summary>
|
||||
Fetches a page of entries from the HubCache table.
|
||||
The page index parameter is zero-based.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateGetDeviceSubscriptionsPagedProcedureQuery">
|
||||
<summary>
|
||||
Fetches a page of device subscriptions.
|
||||
The page index parameter is zero-based.
|
||||
Results are ordered by deviceId and subscriptionType.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Management.EncryptionSetup">
|
||||
<summary>
|
||||
Encryption setup is responsible for creating encryption keys, and re-encrypting sensitive data in the database.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Management.EncryptionSetup.Reencrypt">
|
||||
<summary>
|
||||
Creates and saves a new encryption key in the database.
|
||||
Reencrypts all callback URL's in the database.
|
||||
</summary>
|
||||
<returns>Empty task.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Models.DeviceSubscriptionType.FromString(System.String)">
|
||||
<summary>
|
||||
Returns the corresponding singleton for a give subscription type.
|
||||
</summary>
|
||||
<exception cref="T:DeviceBridge.Common.Exceptions.UnknownDeviceSubscriptionTypeException">If the given value is not a valid subscription type.</exception>
|
||||
<param name="value">The string representation subscription type.</param>
|
||||
<returns>The corresponding singleton for the subscription type.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Models.DeviceSubscriptionType.IsDataSubscription">
|
||||
<summary>
|
||||
A data subscription deals with events that depend on a device connection (properties, methods, C2D messages).
|
||||
A connection status subscription, in the other hand, is just a subscription to engine events that is always active and doesn't depend on a connection.
|
||||
</summary>
|
||||
<returns>Whether this is a data subscription or not.</returns>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Models.ReceiveMessageCallbackStatus">
|
||||
<summary>Enum ReceiveMessageCallbackStatus.</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Models.ReceiveMessageCallbackStatus.Accept">
|
||||
<summary>Client should accept message.</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Models.ReceiveMessageCallbackStatus.Reject">
|
||||
<summary>Client should reject message.</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Models.ReceiveMessageCallbackStatus.Abandon">
|
||||
<summary>Client should abandon message.</summary>
|
||||
</member>
|
||||
<member name="F:DeviceBridge.Providers.StorageProvider.TableNotFoundErrorNumber">
|
||||
<summary>
|
||||
Taken from https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.ListAllSubscriptionsOrderedByDeviceId(NLog.Logger)">
|
||||
<summary>
|
||||
Lists all active subscriptions of all types ordered by device Id.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<returns>List of all subscriptions of all types ordered by device Id.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.ListDeviceSubscriptions(NLog.Logger,System.String)">
|
||||
<summary>
|
||||
Lists all active subscriptions of all types for a device.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<param name="deviceId">Id of the device to get the subscriptions for.</param>
|
||||
<returns>List of subscriptions for the given device.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.GetDeviceSubscription(NLog.Logger,System.String,DeviceBridge.Models.DeviceSubscriptionType,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Gets an active subscription of the specified type for a device, if one exists.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<param name="deviceId">Id of the device to get the subscription for.</param>
|
||||
<param name="subscriptionType">Type of the subscription to get.</param>
|
||||
<param name="cancellationToken">Cancellation token.</param>
|
||||
<returns>The subscription, if exists. Null otherwise.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.CreateOrUpdateDeviceSubscription(NLog.Logger,System.String,DeviceBridge.Models.DeviceSubscriptionType,System.String,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Creates a subscription of the given type for the given device. If one already exists, it's updated with a new creation time and callback URL.
|
||||
Returns the created or updated subscription.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<param name="deviceId">Id of the device to create the subscription for.</param>
|
||||
<param name="subscriptionType">Type of the subscription to be created.</param>
|
||||
<param name="callbackUrl">Callback URL of the subscription.</param>
|
||||
<param name="cancellationToken">Cancellation token.</param>
|
||||
<returns>The created subscription.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.DeleteDeviceSubscription(NLog.Logger,System.String,DeviceBridge.Models.DeviceSubscriptionType,System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
Deletes the subscription of the given type for a device, if one exists.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<param name="deviceId">Id of the device to delete the subscription for.</param>
|
||||
<param name="subscriptionType">Type of the subscription to be deleted.</param>
|
||||
<param name="cancellationToken">Cancellation token.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.GcHubCache(NLog.Logger)">
|
||||
<summary>
|
||||
Deletes from the hub cache any device that doesn't have a subscription and hasn't attempted to open a connection in the past week.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.RenewHubCacheEntries(NLog.Logger,System.Collections.Generic.List{System.String})">
|
||||
<summary>
|
||||
Renews the Hub cache timestamp for a list of devices.
|
||||
</summary>
|
||||
<param name="logger">The logger instance to use.</param>
|
||||
<param name="deviceIds">List of device Ids to renew.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.AddOrUpdateHubCacheEntry(NLog.Logger,System.String,System.String)">
|
||||
<summary>
|
||||
Adds or updates a Hub cache entry for a device.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<param name="deviceId">Id of the device for the new cache entry.</param>
|
||||
<param name="hub">Hub to be added to the cache entry for the device.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.ListHubCacheEntries(NLog.Logger)">
|
||||
<summary>
|
||||
Lists all entries in the Hub cache.
|
||||
</summary>
|
||||
<param name="logger">Logger to be used.</param>
|
||||
<returns>List of all entries in the DB hub cache.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.Exec(NLog.Logger,System.String)">
|
||||
<summary>
|
||||
Executes an arbitrary SQL command against the DB.
|
||||
</summary>
|
||||
<param name="logger">Logger instance to use.</param>
|
||||
<param name="sql">SQL command to run.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Providers.StorageProvider.TranslateSqlException(System.Exception)">
|
||||
<summary>
|
||||
Translates SQL exceptions into service exceptions.
|
||||
</summary>
|
||||
<param name="e">Original SQL exception.</param>
|
||||
<returns>The translated service exception.</returns>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Services.ConnectionManager">
|
||||
<summary>
|
||||
Manages SDK device connections. A connection can have two modes: permanent or temporary.
|
||||
|
||||
A permanent connection is one that should be kept open indefinitely, until the user explicitly decides to close it.
|
||||
This connection type is used for any type of persistent subscription that needs an always-on connection, such as desired property changes.
|
||||
|
||||
A temporary connection is used for point-in-time operations, such as sending telemetry and getting the current device twin.
|
||||
This type of connection lives for a few minutes (currently 9-10 mins) and is automatically closed. It's used to increase the chances
|
||||
of a connection being already open when a point-in-time operation happens but also to make sure that connections don't stay
|
||||
open for too long for silent devices.
|
||||
|
||||
Temporary connections are rewed whenever a new operation happens. Deleting a permanent connection falls back to a temporary connection if one exists.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.StartExpiredConnectionCleanupAsync">
|
||||
<summary>
|
||||
Attempts to cleanup expired temporary connections every 10 seconds.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.GetDeviceStatus(System.String)">
|
||||
<summary>
|
||||
See <see href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet">ConnectionStatus documentation</see>
|
||||
for a detailed description of each status and reaosn.
|
||||
</summary>
|
||||
<param name="deviceId">Id of the device to get the status for.</param>
|
||||
<returns>The last known connection status of the device or null if the device has never connected.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.GetDevicesThatConnectedSince(System.DateTime)">
|
||||
<summary>
|
||||
Gets the list of all devices that attempted to connect since a given timestamp.
|
||||
</summary>
|
||||
<param name="threshold">Timestamp to filter by.</param>
|
||||
<returns>The list of device Ids that attempted to connect since the given timestamp.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.AssertDeviceConnectionOpenAsync(System.String,System.Boolean,System.Boolean,System.Nullable{System.Threading.CancellationToken})">
|
||||
<summary>
|
||||
Asserts that a permanent or temporary connection for this device is open.
|
||||
A temporary connection is guaranteed to live for only a few minutes (currently 9-11 minutes).
|
||||
</summary>
|
||||
<param name="deviceId">Id of the device to open a connection for.</param>
|
||||
<param name="temporary">Whether the requested connection is temporary or permanent.</param>
|
||||
<param name="recreateFailedClient">Forces the recreation of the current client if it's in a permanent failure state (i.e., the SDK will not retry).</param>
|
||||
<param name="cancellationToken">Optional cancellation token.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.AssertDeviceConnectionClosedAsync(System.String,System.Boolean)">
|
||||
<summary>
|
||||
Asserts that the permanent or temporary connection for this device is closed. The temporary connection is only closed
|
||||
if it has expired. The underlying connection is not actually closed if we're trying to delete a permanent connection and
|
||||
a temporary one exists or vice-versa.
|
||||
</summary>
|
||||
<param name="deviceId">Id of the decide for which the connection should be closed.</param>
|
||||
<param name="temporary">Whether the temporary or permanent connection should be closed.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.StandaloneDpsRegistrationAsync(NLog.Logger,System.String,System.String,System.Nullable{System.Threading.CancellationToken})">
|
||||
<summary>
|
||||
Performs a standalone DPS registration (not part of a device connection). The registration data is cached for future connections.
|
||||
</summary>
|
||||
<param name="logger">Logger instance to use.</param>
|
||||
<param name="deviceId">Id of the device to register.</param>
|
||||
<param name="modelId">Optional model Id to assign the device to.</param>
|
||||
<param name="cancellationToken">Optional cancellation token.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.SetDesiredPropertyUpdateCallbackAsync(System.String,System.String,Microsoft.Azure.Devices.Client.DesiredPropertyUpdateCallback)">
|
||||
<summary>
|
||||
Sets the desired property change callback. The callback is not tied to a connection lifetime and will be active whenever the device
|
||||
status is marked as online.
|
||||
</summary>
|
||||
<param name="deviceId">Id to the device to set the callback for.</param>
|
||||
<param name="id">string identifying the callback, for tracking purposes.</param>
|
||||
<param name="callback">The callback to be called when a desired property update is received.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.SetMethodCallbackAsync(System.String,System.String,Microsoft.Azure.Devices.Client.MethodCallback)">
|
||||
<summary>
|
||||
Sets the direct method callback for a device. The callback is not tied to a connection lifetime and will be active whenever the device
|
||||
status is marked as online.
|
||||
</summary>
|
||||
<param name="deviceId">Id to the device to set the callback for.</param>
|
||||
<param name="id">string identifying the callback, for tracking purposes.</param>
|
||||
<param name="callback">The callback to be called when a method invocation is received.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.SetMessageCallbackAsync(System.String,System.String,System.Func{Microsoft.Azure.Devices.Client.Message,System.Threading.Tasks.Task{DeviceBridge.Models.ReceiveMessageCallbackStatus}})">
|
||||
<summary>
|
||||
Sets the direct message callback for a device. The callback is not tied to a connection lifetime and will be active whenever the device
|
||||
status is marked as online.
|
||||
</summary>
|
||||
<param name="deviceId">Id to the device to set the callback for.</param>
|
||||
<param name="id">string identifying the callback, for tracking purposes.</param>
|
||||
<param name="callback">The callback to be called when a C2D message is received.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.SetConnectionStatusCallback(System.String,System.Func{Microsoft.Azure.Devices.Client.ConnectionStatus,Microsoft.Azure.Devices.Client.ConnectionStatusChangeReason,System.Threading.Tasks.Task})">
|
||||
<summary>
|
||||
Sets the connection status change handler for a device.
|
||||
</summary>
|
||||
<param name="deviceId">Id of the device to set the callback for.</param>
|
||||
<param name="callback">Callback to be called when the status of the device connection changes.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.Dispose">
|
||||
<summary>
|
||||
Attempts to gracefully shutdown all SDK connections.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.DpsRegisterInternalAsync(NLog.Logger,System.String,System.String,System.String,System.Nullable{System.Threading.CancellationToken})">
|
||||
<summary>
|
||||
Internal wrapper for DPS registration.
|
||||
</summary>
|
||||
<exception cref="T:DeviceBridge.Common.Exceptions.DpsRegistrationFailedWithUnknownStatusException">If the final registration status is not "assigned".</exception>
|
||||
<param name="logger">Logger instance to use.</param>
|
||||
<param name="deviceId">Id of the device to register.</param>
|
||||
<param name="deviceKey">Key for the device.</param>
|
||||
<param name="modelId">Optional model Id to be passed to DPS.</param>
|
||||
<param name="cancellationToken">Optional cancellation token.</param>
|
||||
<returns>The assigned hub for this device.</returns>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.ConnectionManager.BuildConnectionStatusChangeHandler(System.String)">
|
||||
<summary>
|
||||
Builds a connection change handler for a specific deviceId, which optionally calls a custom callback.
|
||||
</summary>
|
||||
<param name="deviceId">Id of the device for which to build the callback.</param>
|
||||
<returns>The connection status change handler.</returns>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Services.CustomDeviceClientRetryPolicy">
|
||||
<summary>
|
||||
Extends the default SDK retry policy (ExponentialBackoff) to fail right away if the hub doesn't exist.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.CustomDeviceClientRetryPolicy.ShouldRetry(System.Int32,System.Exception,System.TimeSpan@)">
|
||||
<summary>
|
||||
Returns true if, based on the parameters, the operation should be retried.
|
||||
</summary>
|
||||
<param name="currentRetryCount">How many times the operation has been retried.</param>
|
||||
<param name="lastException">Operation exception.</param>
|
||||
<param name="retryInterval">Next retry should be performed after this time interval.</param>
|
||||
<returns>True if the operation should be retried, false otherwise.</returns>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Services.ExpiredConnectionCleanupHostedService">
|
||||
<summary>
|
||||
When the application starts, start the expired connection cleanup task.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Services.HubCacheGcHostedService">
|
||||
<summary>
|
||||
Every 6 hours:
|
||||
- renews the Hub cache entries for the devices that attempted to open a connection.
|
||||
- runs the GC routine in the Hub cache, removing entries for any device that doesn't have a subscription and
|
||||
hasn't connected in the last week.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.SubscriptionService.StartDataSubscriptionsInitializationAsync">
|
||||
<summary>
|
||||
Starts the Initialization of data subscriptions for all devices based on the list fetched from the DB at service construction time.
|
||||
For use during service startup.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.SubscriptionService.SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(System.String,System.Boolean,System.Boolean)">
|
||||
<summary>
|
||||
Asserts that the internal engine state (connection and callbacks) for a device reflects the subscriptions status and callbacks stored in the DB or in the initialization list.
|
||||
Only applies to data subscriptions, i.e., not connection status subscriptions.
|
||||
</summary>
|
||||
<param name="deviceId">Id of the device to synchronize subscriptions for.</param>
|
||||
<param name="useInitializationList">Whether subscriptions should be pulled from the initialization list or fetched from the DB.</param>
|
||||
<param name="forceConnectionRetry">Whether to force a connection retry if the current connection is in a failed state.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Services.SubscriptionService.ComputeDataSubscriptionStatus(System.String,DeviceBridge.Models.DeviceSubscriptionType,System.String)">
|
||||
<summary>
|
||||
Determines the status of a subscription based on the current state of the device client.
|
||||
</summary>
|
||||
<param name="deviceId">Id of the device for which to check the subscription status.</param>
|
||||
<param name="subscriptionType">Type of the subscription that we want the status for.</param>
|
||||
<param name="callbackUrl">URL for which we want to check the subscription status.</param>
|
||||
<returns>Status of the subscription.</returns>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Services.SubscriptionStartupHostedService">
|
||||
<summary>
|
||||
When the application starts, initialize all device subscriptions that we have in the DB.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:DeviceBridge.Startup">
|
||||
<summary>Class Startup.</summary>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Startup.#ctor(Microsoft.Extensions.Configuration.IConfiguration)">
|
||||
<summary>Initializes a new instance of the <see cref="T:DeviceBridge.Startup"/> class.</summary>
|
||||
<param name="configuration">The configuration.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Startup.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)">
|
||||
<summary>This method gets called by the runtime. Use this method to add services to the container.</summary>
|
||||
<param name="services">The services.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Startup.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Hosting.IWebHostEnvironment,Microsoft.Extensions.Hosting.IHostApplicationLifetime,DeviceBridge.Services.ConnectionManager)">
|
||||
<summary>This method gets called by the runtime. Use this method to configure the HTTP request pipeline..</summary>
|
||||
<param name="app">The application.</param>
|
||||
<param name="env">The env.</param>
|
||||
<param name="lifetime">The lifetime.</param>
|
||||
<param name="connectionManager">The connection manager.</param>
|
||||
</member>
|
||||
<member name="M:DeviceBridge.Startup.GetRetryPolicy">
|
||||
<summary>
|
||||
<para>Gets the retry policy, used in HttpClient.</para>
|
||||
</summary>
|
||||
<returns>IAsyncPolicy<HttpResponseMessage>.</returns>
|
||||
</member>
|
||||
<member name="M:Utils.GuidFromString(System.String)">
|
||||
<summary>
|
||||
Generates a GUID hashed from an input string.
|
||||
</summary>
|
||||
<param name="input">Input to generate the GUID from.</param>
|
||||
<returns>GUID hashed from input.</returns>
|
||||
</member>
|
||||
<member name="M:Utils.GetSqlConnectionString(NLog.Logger,DeviceBridge.Providers.SecretsProvider)">
|
||||
<summary>
|
||||
Fetches the sql connection string.
|
||||
</summary>
|
||||
<param name="logger">Logger.</param>
|
||||
<param name="secretsProvider">Secrets provider for retrieving credentials.</param>
|
||||
<returns>The sql connection string.</returns>
|
||||
</member>
|
||||
<member name="T:NotFoundResultFilterAttribute">
|
||||
<summary>
|
||||
Converts a null return value into a 404.
|
||||
</summary>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1604:Element documentation should have summary", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1615:Element return value should be documented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:Partial elements should be documented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Ok to omit this prefix")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Conflicts with VS rule")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Using underscore for private class fields")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "Prevents the use of underscore for unused local variable")]
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Providers;
|
||||
using DeviceBridge.Services;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Management
|
||||
{
|
||||
public class DbSchemaSetup
|
||||
{
|
||||
private const string CreateDeviceSubscriptionsTableQuery =
|
||||
@"IF OBJECT_ID('dbo.DeviceSubscriptions', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE DeviceSubscriptions(
|
||||
DeviceId VARCHAR(255),
|
||||
SubscriptionType VARCHAR(20),
|
||||
CallbackUrl NVARCHAR(MAX) NOT NULL,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
|
||||
CONSTRAINT pk_device_subscriptions PRIMARY KEY (DeviceId, SubscriptionType)
|
||||
);
|
||||
END";
|
||||
|
||||
private const string CreateHubCacheTableQuery =
|
||||
@"IF OBJECT_ID('dbo.HubCache', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE HubCache(
|
||||
DeviceId VARCHAR(255) PRIMARY KEY,
|
||||
Hub VARCHAR(255) NOT NULL,
|
||||
RenewedAt DATETIME NOT NULL
|
||||
);
|
||||
END";
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a device subscription. If one already exists, updates it.
|
||||
/// Concurrent calls to this procedure will not generate a failure.
|
||||
/// Outputs the creation time.
|
||||
/// </summary>
|
||||
private const string CreateUpsertDeviceSubscriptionProcedureQuery =
|
||||
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.upsertDeviceSubscription'))
|
||||
BEGIN
|
||||
EXEC(N'
|
||||
CREATE PROCEDURE upsertDeviceSubscription
|
||||
@DeviceId VARCHAR(255),
|
||||
@SubscriptionType VARCHAR(20),
|
||||
@CallbackUrl NVARCHAR(MAX),
|
||||
@CreatedAt DATETIME OUTPUT
|
||||
AS
|
||||
DECLARE @CurrentTime DATETIME;
|
||||
SET @CurrentTime = GETUTCDATE();
|
||||
SET @CreatedAt = @CurrentTime;
|
||||
BEGIN TRY
|
||||
INSERT INTO DeviceSubscriptions(DeviceId, SubscriptionType, CallbackUrl, CreatedAt) VALUES(@DeviceId, @SubscriptionType, @CallbackUrl, @CurrentTime);
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF ERROR_NUMBER() = 2627 -- Primary key violation
|
||||
-- This update is a best-effort attempt, i.e., the subscription might have been deleted between the test and set.
|
||||
UPDATE DeviceSubscriptions SET CallbackUrl = @CallbackUrl, CreatedAt = @CurrentTime WHERE DeviceId = @DeviceId AND SubscriptionType = @SubscriptionType;
|
||||
END CATCH
|
||||
');
|
||||
END";
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a hub cache entry for a device. If one already exists, updates it.
|
||||
/// </summary>
|
||||
private const string CreateUpsertHubCacheEntryProcedureQuery =
|
||||
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.upsertHubCacheEntry'))
|
||||
BEGIN
|
||||
EXEC(N'
|
||||
CREATE PROCEDURE upsertHubCacheEntry
|
||||
@DeviceId VARCHAR(255),
|
||||
@Hub VARCHAR(255)
|
||||
AS
|
||||
BEGIN TRY
|
||||
INSERT INTO HubCache(DeviceId, Hub, RenewedAt) VALUES(@DeviceId, @Hub, GETUTCDATE());
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF ERROR_NUMBER() = 2627 -- Primary key violation
|
||||
UPDATE HubCache SET Hub = @Hub, RenewedAt = GETUTCDATE() WHERE DeviceId = @DeviceId;
|
||||
END CATCH
|
||||
');
|
||||
END";
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a page of entries from the HubCache table.
|
||||
/// The page index parameter is zero-based.
|
||||
/// </summary>
|
||||
private const string CreateGetHubCacheEntriesPagedProcedureQuery =
|
||||
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.getHubCacheEntriesPaged'))
|
||||
BEGIN
|
||||
EXEC(N'
|
||||
CREATE PROCEDURE getHubCacheEntriesPaged
|
||||
@PageIndex INT,
|
||||
@RowsPerPage INT
|
||||
AS
|
||||
SELECT * FROM HubCache
|
||||
ORDER BY DeviceId
|
||||
OFFSET @PageIndex*@RowsPerPage ROWS
|
||||
FETCH NEXT @RowsPerPage ROWS ONLY
|
||||
');
|
||||
END";
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a page of device subscriptions.
|
||||
/// The page index parameter is zero-based.
|
||||
/// Results are ordered by deviceId and subscriptionType.
|
||||
/// </summary>
|
||||
private const string CreateGetDeviceSubscriptionsPagedProcedureQuery =
|
||||
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.getDeviceSubscriptionsPaged'))
|
||||
BEGIN
|
||||
EXEC(N'
|
||||
CREATE PROCEDURE getDeviceSubscriptionsPaged
|
||||
@PageIndex INT,
|
||||
@RowsPerPage INT
|
||||
AS
|
||||
SELECT * FROM DeviceSubscriptions
|
||||
ORDER BY DeviceId, SubscriptionType
|
||||
OFFSET @PageIndex* @RowsPerPage ROWS
|
||||
FETCH NEXT @RowsPerPage ROWS ONLY
|
||||
');
|
||||
END";
|
||||
|
||||
public async Task SetupDbSchema()
|
||||
{
|
||||
Logger logger = LogManager.GetCurrentClassLogger();
|
||||
logger.Info("Initializing Key Vault and storage service.");
|
||||
|
||||
string kvUrl = Environment.GetEnvironmentVariable("KV_URL");
|
||||
var secretsProvider = new SecretsProvider(kvUrl);
|
||||
|
||||
// Build connection string.
|
||||
var sqlConnectionString = Utils.GetSqlConnectionString(logger, secretsProvider);
|
||||
var encryptionService = new EncryptionService(logger, secretsProvider);
|
||||
var storageProvider = new StorageProvider(sqlConnectionString, encryptionService);
|
||||
|
||||
// Run schema scripts.
|
||||
logger.Info("Running DB schema setup scripts.");
|
||||
|
||||
logger.Info("Creating DeviceSubscriptions table");
|
||||
await storageProvider.Exec(logger, CreateDeviceSubscriptionsTableQuery);
|
||||
|
||||
logger.Info("Creating HubCache table");
|
||||
await storageProvider.Exec(logger, CreateHubCacheTableQuery);
|
||||
|
||||
logger.Info("Creating UpsertDeviceSubscription stored procedure");
|
||||
await storageProvider.Exec(logger, CreateUpsertDeviceSubscriptionProcedureQuery);
|
||||
|
||||
logger.Info("Creating UpsertHubCacheEntry stored procedure");
|
||||
await storageProvider.Exec(logger, CreateUpsertHubCacheEntryProcedureQuery);
|
||||
|
||||
logger.Info("Creating GetHubCacheEntriesPaged stored procedure");
|
||||
await storageProvider.Exec(logger, CreateGetHubCacheEntriesPagedProcedureQuery);
|
||||
|
||||
logger.Info("Creating GetDeviceSubscriptionsPaged stored procedure");
|
||||
await storageProvider.Exec(logger, CreateGetDeviceSubscriptionsPagedProcedureQuery);
|
||||
|
||||
logger.Info("Successfully executed DB schema setup.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Providers;
|
||||
using DeviceBridge.Services;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Management
|
||||
{
|
||||
/// <summary>
|
||||
/// Encryption setup is responsible for creating encryption keys, and re-encrypting sensitive data in the database.
|
||||
/// </summary>
|
||||
public class EncryptionSetup
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and saves a new encryption key in the database.
|
||||
/// Reencrypts all callback URL's in the database.
|
||||
/// </summary>
|
||||
/// <returns>Empty task.</returns>
|
||||
public async Task Reencrypt()
|
||||
{
|
||||
Logger logger = LogManager.GetCurrentClassLogger();
|
||||
logger.Info("Starting re-encryption.");
|
||||
|
||||
var kvUrl = Environment.GetEnvironmentVariable("KV_URL");
|
||||
var secretsService = new SecretsProvider(kvUrl);
|
||||
var sqlConnectionString = Utils.GetSqlConnectionString(logger, secretsService);
|
||||
var secretsProvider = new SecretsProvider(kvUrl);
|
||||
var encryptionService = new EncryptionService(logger, secretsProvider);
|
||||
var storageProvider = new StorageProvider(sqlConnectionString, encryptionService);
|
||||
var subs = await storageProvider.ListAllSubscriptionsOrderedByDeviceId(logger);
|
||||
|
||||
// Generate new key
|
||||
await secretsProvider.PutEncyptionKey(logger, System.Text.Encoding.ASCII.GetString(Aes.Create().Key));
|
||||
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
await storageProvider.CreateOrUpdateDeviceSubscription(logger, sub.DeviceId, sub.SubscriptionType, sub.CallbackUrl, CancellationToken.None);
|
||||
}
|
||||
|
||||
logger.Info("Re-encryption complete.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class C2DMessageInvocationEventBody
|
||||
{
|
||||
[JsonProperty("eventType")]
|
||||
public const string EventType = "C2DMessage";
|
||||
|
||||
[JsonProperty("deviceId")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("deviceReceivedAt")]
|
||||
public DateTime DeviceReceivedAt { get; set; }
|
||||
|
||||
[JsonProperty("messageBody")]
|
||||
public JRaw MessageBody { get; set; }
|
||||
|
||||
[JsonProperty("properties")]
|
||||
public IDictionary<string, string> Properties { get; set; }
|
||||
|
||||
[JsonProperty("messageId")]
|
||||
public string MessageId { get; set; }
|
||||
|
||||
[JsonProperty("expirtyTimeUtC")]
|
||||
public DateTime ExpiryTimeUTC { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class ConnectionStatusChangeEventBody
|
||||
{
|
||||
[JsonProperty("eventType")]
|
||||
public const string EventType = "ConnectionStatusChange";
|
||||
|
||||
[JsonProperty("deviceId")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("deviceReceivedAt")]
|
||||
public DateTime DeviceReceivedAt { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("reason")]
|
||||
public string Reason { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class DesiredPropertyUpdateEventBody
|
||||
{
|
||||
[JsonProperty("eventType")]
|
||||
public const string EventType = "DesiredPropertyUpdate";
|
||||
|
||||
[JsonProperty("deviceId")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("deviceReceivedAt")]
|
||||
public DateTime DeviceReceivedAt { get; set; }
|
||||
|
||||
[JsonProperty("desiredProperties")]
|
||||
public JRaw DesiredProperties { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class DeviceStatusResponseBody
|
||||
{
|
||||
public string Status { get; set; }
|
||||
|
||||
public string Reason { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class DeviceSubscription
|
||||
{
|
||||
public DeviceSubscription()
|
||||
{
|
||||
}
|
||||
|
||||
public DeviceSubscription(DeviceSubscription original)
|
||||
{
|
||||
DeviceId = original.DeviceId;
|
||||
SubscriptionType = original.SubscriptionType;
|
||||
CallbackUrl = original.CallbackUrl;
|
||||
CreatedAt = original.CreatedAt;
|
||||
}
|
||||
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
public DeviceSubscriptionType SubscriptionType { get; set; }
|
||||
|
||||
public string CallbackUrl { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using DeviceBridge.Common.Exceptions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
[JsonConverter(typeof(DeviceSubscriptionTypeConverter))]
|
||||
public class DeviceSubscriptionType
|
||||
{
|
||||
public static readonly OpenApiSchema Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
};
|
||||
|
||||
public static readonly DeviceSubscriptionType DesiredProperties = new DeviceSubscriptionType(DesiredPropertiesSubscriptionType);
|
||||
public static readonly DeviceSubscriptionType Methods = new DeviceSubscriptionType(MethodsSubscriptionType);
|
||||
public static readonly DeviceSubscriptionType C2DMessages = new DeviceSubscriptionType(C2DSubscriptionType);
|
||||
public static readonly DeviceSubscriptionType ConnectionStatus = new DeviceSubscriptionType(ConnectionStatusSubscriptionType);
|
||||
|
||||
private const string DesiredPropertiesSubscriptionType = "DesiredProperties";
|
||||
private const string MethodsSubscriptionType = "Methods";
|
||||
private const string C2DSubscriptionType = "C2DMessages";
|
||||
private const string ConnectionStatusSubscriptionType = "ConnectionStatus";
|
||||
|
||||
private string value;
|
||||
|
||||
private DeviceSubscriptionType(string value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case DesiredPropertiesSubscriptionType:
|
||||
case MethodsSubscriptionType:
|
||||
case C2DSubscriptionType:
|
||||
case ConnectionStatusSubscriptionType:
|
||||
this.value = value;
|
||||
break;
|
||||
default:
|
||||
throw new UnknownDeviceSubscriptionTypeException(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool operator ==(DeviceSubscriptionType lhs, DeviceSubscriptionType rhs)
|
||||
{
|
||||
return (ReferenceEquals(lhs, null) && ReferenceEquals(rhs, null)) || (!ReferenceEquals(lhs, null) && lhs.Equals(rhs));
|
||||
}
|
||||
|
||||
public static bool operator !=(DeviceSubscriptionType lhs, DeviceSubscriptionType rhs)
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the corresponding singleton for a give subscription type.
|
||||
/// </summary>
|
||||
/// <exception cref="UnknownDeviceSubscriptionTypeException">If the given value is not a valid subscription type.</exception>
|
||||
/// <param name="value">The string representation subscription type.</param>
|
||||
/// <returns>The corresponding singleton for the subscription type.</returns>
|
||||
public static DeviceSubscriptionType FromString(string value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case DesiredPropertiesSubscriptionType:
|
||||
return DesiredProperties;
|
||||
case MethodsSubscriptionType:
|
||||
return Methods;
|
||||
case C2DSubscriptionType:
|
||||
return C2DMessages;
|
||||
case ConnectionStatusSubscriptionType:
|
||||
return ConnectionStatus;
|
||||
default:
|
||||
throw new UnknownDeviceSubscriptionTypeException(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A data subscription deals with events that depend on a device connection (properties, methods, C2D messages).
|
||||
/// A connection status subscription, in the other hand, is just a subscription to engine events that is always active and doesn't depend on a connection.
|
||||
/// </summary>
|
||||
/// <returns>Whether this is a data subscription or not.</returns>
|
||||
public bool IsDataSubscription()
|
||||
{
|
||||
return (this == DesiredProperties) || (this == Methods) || (this == C2DMessages);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
public override bool Equals(object type)
|
||||
{
|
||||
if (ReferenceEquals(type, null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ReferenceEquals(this, type) || (value == (type as DeviceSubscriptionType).value);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return value.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class DeviceSubscriptionTypeConverter : JsonConverter<DeviceSubscriptionType>
|
||||
{
|
||||
public override DeviceSubscriptionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DeviceSubscriptionType value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class DeviceSubscriptionWithStatus : DeviceSubscription
|
||||
{
|
||||
public DeviceSubscriptionWithStatus(DeviceSubscription original)
|
||||
: base(original)
|
||||
{
|
||||
}
|
||||
|
||||
public string Status { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class DeviceTwin
|
||||
{
|
||||
public static readonly OpenApiSchema Schema = new OpenApiSchema()
|
||||
{
|
||||
Type = "object",
|
||||
Properties = new Dictionary<string, OpenApiSchema>()
|
||||
{
|
||||
{
|
||||
"twin", new OpenApiSchema()
|
||||
{
|
||||
Type = "object",
|
||||
Properties = new Dictionary<string, OpenApiSchema>()
|
||||
{
|
||||
{
|
||||
"properties", new OpenApiSchema()
|
||||
{
|
||||
Type = "object",
|
||||
Properties = new Dictionary<string, OpenApiSchema>()
|
||||
{
|
||||
{
|
||||
"desired", new OpenApiSchema()
|
||||
{
|
||||
Type = "object",
|
||||
}
|
||||
},
|
||||
{
|
||||
"reported", new OpenApiSchema()
|
||||
{
|
||||
Type = "object",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
[JsonProperty("twin")]
|
||||
public JRaw Twin { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class HubCacheEntry
|
||||
{
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
public string Hub { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class MessageBody
|
||||
{
|
||||
[Required]
|
||||
public IDictionary<string, object> Data { get; set; }
|
||||
|
||||
public IDictionary<string, string> Properties { get; set; }
|
||||
|
||||
public string ComponentName { get; set; }
|
||||
|
||||
public DateTime? CreationTimeUtc { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class MethodInvocationEventBody
|
||||
{
|
||||
[JsonProperty("eventType")]
|
||||
public const string EventType = "DirectMethodInvocation";
|
||||
|
||||
[JsonProperty("deviceId")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("deviceReceivedAt")]
|
||||
public DateTime DeviceReceivedAt { get; set; }
|
||||
|
||||
[JsonProperty("methodName")]
|
||||
public string MethodName { get; set; }
|
||||
|
||||
[JsonProperty("requestData")]
|
||||
public JRaw RequestData { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class MethodResponseBody
|
||||
{
|
||||
public JsonElement? Payload { get; set; }
|
||||
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
/// <summary>Enum ReceiveMessageCallbackStatus.</summary>
|
||||
public enum ReceiveMessageCallbackStatus
|
||||
{
|
||||
/// <summary>Client should accept message.</summary>
|
||||
Accept,
|
||||
|
||||
/// <summary>Client should reject message.</summary>
|
||||
Reject,
|
||||
|
||||
/// <summary>Client should abandon message.</summary>
|
||||
Abandon,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class RegistrationBody
|
||||
{
|
||||
public string ModelId { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class ReportedPropertiesPatch
|
||||
{
|
||||
[Required]
|
||||
public IDictionary<string, object> Patch { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DeviceBridge.Models
|
||||
{
|
||||
public class SubscriptionCreateOrUpdateBody
|
||||
{
|
||||
[Required]
|
||||
public string CallbackUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
|
||||
autoReload="true"
|
||||
throwExceptions="false"
|
||||
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
|
||||
|
||||
<targets>
|
||||
<target name="logconsole" xsi:type="Console" >
|
||||
<layout xsi:type="JsonLayout" includeAllProperties="true" maxRecursionLimit="20" excludeProperties="">
|
||||
<attribute name='time' layout='${longdate}' />
|
||||
<attribute name='level' layout='${level:upperCase=true}'/>
|
||||
<attribute name='message' layout='${message}' />
|
||||
<attribute name='exception' layout='${exception}' />
|
||||
<attribute name='callsite' layout='${callsite}, ${callsite-linenumber}' />
|
||||
</layout>
|
||||
</target>
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
<logger name="*" minlevel="Debug" writeTo="logconsole" />
|
||||
</rules>
|
||||
|
||||
|
||||
</nlog>
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using DeviceBridge.Common.Exceptions;
|
||||
using DeviceBridge.Management;
|
||||
using DeviceBridge.Providers;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NLog.Web;
|
||||
|
||||
namespace DeviceBridge
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>().UseUrls("http://localhost:5001");
|
||||
});
|
||||
}
|
||||
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
|
||||
|
||||
// In setup mode only run setup tasks without bringing up the server.
|
||||
if (args.Contains("--setup"))
|
||||
{
|
||||
logger.Info("Executing in setup mode.");
|
||||
|
||||
try
|
||||
{
|
||||
var dbSchemaSetup = new DbSchemaSetup();
|
||||
dbSchemaSetup.SetupDbSchema().Wait();
|
||||
|
||||
var encryptionSetup = new EncryptionSetup();
|
||||
encryptionSetup.Reencrypt().Wait();
|
||||
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
NLog.LogManager.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// If the storage setup is not complete, wait 30 seconds before exiting. This gives more time for setup to finish before next execution is attempted.
|
||||
if (e.InnerException is StorageSetupIncompleteException)
|
||||
{
|
||||
logger.Info("ERROR: DB setup is not complete. Please make sure that schema setup task finishes successfully. Process will exit in 30 seconds...");
|
||||
Thread.Sleep(30000);
|
||||
}
|
||||
|
||||
logger.Error(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NLog.LogManager.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50744",
|
||||
"sslPort": 44305
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"BridgeV2Service": {
|
||||
"commandName": "Project",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"KV_URL": "<replace-with-local-development-keyvault-url>",
|
||||
"MAX_POOL_SIZE": "50",
|
||||
"DEVICE_RAMPUP_BATCH_SIZE": "150",
|
||||
"DEVICE_RAMPUP_BATCH_INTERVAL_MS": "1000",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUP__KEYVAULT__CONFIGURATIONENABLED": "true",
|
||||
"HTTP_RETRY_LIMIT": "5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"secrets1": {
|
||||
"type": "secrets",
|
||||
"connectionId": "ASPNETCORE_HOSTINGSTARTUP__KEYVAULT__CONFIGURATIONVAULT"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"secrets1": {
|
||||
"type": "secrets.keyVault",
|
||||
"connectionId": "ASPNETCORE_HOSTINGSTARTUP__KEYVAULT__CONFIGURATIONVAULT"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.KeyVault.Models;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Providers
|
||||
{
|
||||
public interface ISecretsProvider
|
||||
{
|
||||
Task PutSecretAsync(Logger logger, string secretName, string secretValue);
|
||||
|
||||
Task<string> GetIdScopeAsync(Logger logger);
|
||||
|
||||
Task<string> GetIotcSasKeyAsync(Logger logger);
|
||||
|
||||
Task<string> GetSqlPasswordAsync(Logger logger);
|
||||
|
||||
Task<string> GetSqlUsernameAsync(Logger logger);
|
||||
|
||||
Task<string> GetSqlServerAsync(Logger logger);
|
||||
|
||||
Task<string> GetSqlDatabaseAsync(Logger logger);
|
||||
|
||||
Task<string> GetApiKey(Logger logger);
|
||||
|
||||
Task<SecretBundle> GetEncryptionKey(Logger logger, string version = null);
|
||||
|
||||
Task<IDictionary<string, SecretBundle>> GetEncryptionKeyVersions(Logger logger);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Providers
|
||||
{
|
||||
public interface IStorageProvider
|
||||
{
|
||||
Task<List<DeviceSubscription>> ListAllSubscriptionsOrderedByDeviceId(Logger logger);
|
||||
|
||||
Task<List<DeviceSubscription>> ListDeviceSubscriptions(Logger logger, string deviceId);
|
||||
|
||||
Task<DeviceSubscription> GetDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken);
|
||||
|
||||
Task<DeviceSubscription> CreateOrUpdateDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl, CancellationToken cancellationToken);
|
||||
|
||||
Task DeleteDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken);
|
||||
|
||||
Task GcHubCache(Logger logger);
|
||||
|
||||
Task RenewHubCacheEntries(Logger logger, List<string> deviceIds);
|
||||
|
||||
Task AddOrUpdateHubCacheEntry(Logger logger, string deviceId, string hub);
|
||||
|
||||
Task<List<HubCacheEntry>> ListHubCacheEntries(Logger logger);
|
||||
|
||||
Task Exec(Logger logger, string sql);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.KeyVault;
|
||||
using Microsoft.Azure.KeyVault.Models;
|
||||
using Microsoft.Azure.Services.AppAuthentication;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Providers
|
||||
{
|
||||
public class SecretsProvider : ISecretsProvider
|
||||
{
|
||||
public const string IotcIdScope = "iotc-id-scope";
|
||||
public const string IotcSasKey = "iotc-sas-key";
|
||||
public const string IotcEncryptionKey = "iotc-encryption-key";
|
||||
public const string SqlServer = "sql-server";
|
||||
public const string SqlPassword = "sql-password";
|
||||
public const string SqlUsername = "sql-username";
|
||||
public const string SqlDatabase = "sql-database";
|
||||
public const string ApiKeyName = "apiKey";
|
||||
|
||||
private readonly KeyVaultClient kvClient;
|
||||
private readonly string kvUrl;
|
||||
private string apiKey;
|
||||
|
||||
public SecretsProvider(string kvUrl)
|
||||
{
|
||||
var tokenProvider = new AzureServiceTokenProvider();
|
||||
kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
|
||||
this.kvUrl = kvUrl;
|
||||
}
|
||||
|
||||
public async Task PutSecretAsync(Logger logger, string secretName, string secretValue)
|
||||
{
|
||||
logger.Info("Adding secret {secretName} to Key Vault", secretName);
|
||||
await kvClient.SetSecretAsync($"{kvUrl}", secretName, secretValue);
|
||||
}
|
||||
|
||||
public async Task<string> GetIdScopeAsync(Logger logger)
|
||||
{
|
||||
return await GetSecretValueAsync(logger, IotcIdScope);
|
||||
}
|
||||
|
||||
public async Task<string> GetIotcSasKeyAsync(Logger logger)
|
||||
{
|
||||
return await GetSecretValueAsync(logger, IotcSasKey);
|
||||
}
|
||||
|
||||
public async Task<string> GetSqlPasswordAsync(Logger logger)
|
||||
{
|
||||
return await GetSecretValueAsync(logger, SqlPassword);
|
||||
}
|
||||
|
||||
public async Task<string> GetSqlUsernameAsync(Logger logger)
|
||||
{
|
||||
return await GetSecretValueAsync(logger, SqlUsername);
|
||||
}
|
||||
|
||||
public async Task<string> GetSqlServerAsync(Logger logger)
|
||||
{
|
||||
return await GetSecretValueAsync(logger, SqlServer);
|
||||
}
|
||||
|
||||
public async Task<string> GetSqlDatabaseAsync(Logger logger)
|
||||
{
|
||||
return await GetSecretValueAsync(logger, SqlDatabase);
|
||||
}
|
||||
|
||||
public async Task<string> GetApiKey(Logger logger)
|
||||
{
|
||||
if (apiKey == null)
|
||||
{
|
||||
apiKey = await GetSecretValueAsync(logger, ApiKeyName);
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public async Task<SecretBundle> GetEncryptionKey(Logger logger, string version = null)
|
||||
{
|
||||
return await GetSecretAsync(logger, IotcEncryptionKey, version);
|
||||
}
|
||||
|
||||
public async Task PutEncyptionKey(Logger logger, string value)
|
||||
{
|
||||
await PutSecretAsync(logger, IotcEncryptionKey, value);
|
||||
}
|
||||
|
||||
public async Task<IDictionary<string, SecretBundle>> GetEncryptionKeyVersions(Logger logger)
|
||||
{
|
||||
var versions = await kvClient.GetSecretVersionsAsync(kvUrl, IotcEncryptionKey);
|
||||
|
||||
var secrets = new Dictionary<string, SecretBundle>();
|
||||
|
||||
do
|
||||
{
|
||||
foreach (var version in versions)
|
||||
{
|
||||
secrets.Add(version.Identifier.Version, await GetEncryptionKey(logger, version.Identifier.Version));
|
||||
}
|
||||
}
|
||||
while (versions.NextPageLink != null && (versions = await kvClient.GetSecretVersionsNextAsync(versions.NextPageLink)) != null);
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
private async Task<string> GetSecretValueAsync(Logger logger, string secretName, string secretVersion = null)
|
||||
{
|
||||
return (await GetSecretAsync(logger, secretName, secretVersion)).Value;
|
||||
}
|
||||
|
||||
private async Task<SecretBundle> GetSecretAsync(Logger logger, string secretName, string secretVersion = null)
|
||||
{
|
||||
logger.Info("Getting secret {secretName} from Key Vault", secretName);
|
||||
|
||||
return (secretVersion == null) ? await kvClient.GetSecretAsync($"{kvUrl}", secretName) : await kvClient.GetSecretAsync($"{kvUrl}", secretName, secretVersion);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,451 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Common.Exceptions;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Providers
|
||||
{
|
||||
public class StorageProvider : IStorageProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Taken from https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors.
|
||||
/// </summary>
|
||||
private const int TableNotFoundErrorNumber = 208;
|
||||
private const int StoredProcedureNotFoundErrorNumber = 2812;
|
||||
|
||||
private const int DefaultPageSIze = 1000;
|
||||
private const int BulkCopyBatchTimeout = 60;
|
||||
private const int BulkCopyBatchSize = 1000;
|
||||
|
||||
private readonly string _connectionString;
|
||||
private readonly EncryptionService _encryptionService;
|
||||
|
||||
public StorageProvider(string connectionString, EncryptionService encryptionService)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_encryptionService = encryptionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all active subscriptions of all types ordered by device Id.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <returns>List of all subscriptions of all types ordered by device Id.</returns>
|
||||
public async Task<List<DeviceSubscription>> ListAllSubscriptionsOrderedByDeviceId(Logger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Getting all subscriptions in the DB");
|
||||
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
var subscriptions = new List<DeviceSubscription>();
|
||||
int lastPageSize;
|
||||
int pageIndex = 0;
|
||||
|
||||
do
|
||||
{
|
||||
logger.Info("Fetching page {pageIndex} of device subscriptions", pageIndex);
|
||||
|
||||
using SqlCommand command = new SqlCommand("getDeviceSubscriptionsPaged", connection)
|
||||
{
|
||||
CommandType = CommandType.StoredProcedure,
|
||||
};
|
||||
|
||||
command.Parameters.Add(new SqlParameter("@PageIndex", pageIndex++));
|
||||
command.Parameters.Add(new SqlParameter("@RowsPerPage", DefaultPageSIze));
|
||||
|
||||
using SqlDataReader reader = await command.ExecuteReaderAsync();
|
||||
var itemCountBeforePage = subscriptions.Count;
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
subscriptions.Add(new DeviceSubscription()
|
||||
{
|
||||
DeviceId = reader["DeviceId"].ToString(),
|
||||
SubscriptionType = DeviceSubscriptionType.FromString(reader["SubscriptionType"].ToString()),
|
||||
CallbackUrl = await _encryptionService.Decrypt(logger, reader["CallbackUrl"].ToString()),
|
||||
CreatedAt = (DateTime)reader["CreatedAt"],
|
||||
});
|
||||
}
|
||||
|
||||
lastPageSize = subscriptions.Count - itemCountBeforePage;
|
||||
}
|
||||
while (lastPageSize > 0);
|
||||
|
||||
logger.Info("Found {subscriptionCount} subscriptions", subscriptions.Count);
|
||||
return subscriptions;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all active subscriptions of all types for a device.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <param name="deviceId">Id of the device to get the subscriptions for.</param>
|
||||
/// <returns>List of subscriptions for the given device.</returns>
|
||||
public async Task<List<DeviceSubscription>> ListDeviceSubscriptions(Logger logger, string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Getting all subscriptions for device {deviceId}", deviceId);
|
||||
var sql = "SELECT * FROM DeviceSubscriptions WHERE DeviceId = @DeviceId";
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand(sql, connection);
|
||||
command.Parameters.Add(new SqlParameter("DeviceId", deviceId));
|
||||
|
||||
await connection.OpenAsync();
|
||||
using SqlDataReader reader = await command.ExecuteReaderAsync();
|
||||
|
||||
List<DeviceSubscription> subscriptions = new List<DeviceSubscription>();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
subscriptions.Add(new DeviceSubscription()
|
||||
{
|
||||
DeviceId = reader["DeviceId"].ToString(),
|
||||
SubscriptionType = DeviceSubscriptionType.FromString(reader["SubscriptionType"].ToString()),
|
||||
CallbackUrl = await _encryptionService.Decrypt(logger, reader["CallbackUrl"].ToString()),
|
||||
CreatedAt = (DateTime)reader["CreatedAt"],
|
||||
});
|
||||
}
|
||||
|
||||
logger.Info("Found {subscriptionCount} subscriptions for device {deviceId}", subscriptions.Count, deviceId);
|
||||
return subscriptions;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an active subscription of the specified type for a device, if one exists.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <param name="deviceId">Id of the device to get the subscription for.</param>
|
||||
/// <param name="subscriptionType">Type of the subscription to get.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The subscription, if exists. Null otherwise.</returns>
|
||||
public async Task<DeviceSubscription> GetDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Getting {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
|
||||
var sql = "SELECT * FROM DeviceSubscriptions WHERE DeviceId = @DeviceId AND SubscriptionType = @SubscriptionType";
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand(sql, connection);
|
||||
command.Parameters.Add(new SqlParameter("DeviceId", deviceId));
|
||||
command.Parameters.Add(new SqlParameter("SubscriptionType", subscriptionType.ToString()));
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
|
||||
if (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
logger.Info("Got {subscriptionType} for device {deviceId}", subscriptionType, deviceId);
|
||||
return new DeviceSubscription()
|
||||
{
|
||||
DeviceId = reader["DeviceId"].ToString(),
|
||||
SubscriptionType = DeviceSubscriptionType.FromString(reader["SubscriptionType"].ToString()),
|
||||
CallbackUrl = await _encryptionService.Decrypt(logger, reader["CallbackUrl"].ToString()),
|
||||
CreatedAt = (DateTime)reader["CreatedAt"],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("No {subscriptionType} subscription found for device {deviceId}", subscriptionType, deviceId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a subscription of the given type for the given device. If one already exists, it's updated with a new creation time and callback URL.
|
||||
/// Returns the created or updated subscription.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <param name="deviceId">Id of the device to create the subscription for.</param>
|
||||
/// <param name="subscriptionType">Type of the subscription to be created.</param>
|
||||
/// <param name="callbackUrl">Callback URL of the subscription.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The created subscription.</returns>
|
||||
public async Task<DeviceSubscription> CreateOrUpdateDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Creating or updating {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
|
||||
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand("upsertDeviceSubscription", connection)
|
||||
{
|
||||
CommandType = CommandType.StoredProcedure,
|
||||
};
|
||||
|
||||
command.Parameters.Add(new SqlParameter("@DeviceId", deviceId));
|
||||
command.Parameters.Add(new SqlParameter("@SubscriptionType", subscriptionType.ToString()));
|
||||
command.Parameters.Add(new SqlParameter("@CallbackUrl", await _encryptionService.Encrypt(logger, callbackUrl)));
|
||||
command.Parameters.Add(new SqlParameter("@CreatedAt", SqlDbType.DateTime)).Direction = ParameterDirection.Output;
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
logger.Info("Created or updated {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
|
||||
|
||||
return new DeviceSubscription()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
SubscriptionType = subscriptionType,
|
||||
CallbackUrl = callbackUrl,
|
||||
CreatedAt = (DateTime)command.Parameters["@CreatedAt"].Value,
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the subscription of the given type for a device, if one exists.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <param name="deviceId">Id of the device to delete the subscription for.</param>
|
||||
/// <param name="subscriptionType">Type of the subscription to be deleted.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task DeleteDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Deleting {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
|
||||
var sql = "DELETE FROM DeviceSubscriptions WHERE DeviceId = @DeviceId AND SubscriptionType = @SubscriptionType";
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand(sql, connection);
|
||||
command.Parameters.Add(new SqlParameter("DeviceId", deviceId));
|
||||
command.Parameters.Add(new SqlParameter("SubscriptionType", subscriptionType.ToString()));
|
||||
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
logger.Info("Deleted {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes from the hub cache any device that doesn't have a subscription and hasn't attempted to open a connection in the past week.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
public async Task GcHubCache(Logger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Running Hub cache GC");
|
||||
var sql = @"DELETE c FROM HubCache c
|
||||
LEFT JOIN DeviceSubscriptions s ON s.DeviceId = c.DeviceId
|
||||
WHERE (s.DeviceId IS NULL) AND (c.RenewedAt < DATEADD(day, -7, GETUTCDATE()))";
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand(sql, connection);
|
||||
|
||||
await connection.OpenAsync();
|
||||
var affectedRows = await command.ExecuteNonQueryAsync();
|
||||
logger.Info("Successfully cleaned up {hubCount} Hubs during Hub cache GC", affectedRows);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renews the Hub cache timestamp for a list of devices.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger instance to use.</param>
|
||||
/// <param name="deviceIds">List of device Ids to renew.</param>
|
||||
public async Task RenewHubCacheEntries(Logger logger, List<string> deviceIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Renewing Hub cache entries for {count} devices", deviceIds.Count);
|
||||
|
||||
// Add device Ids to a Data Table that we'll bulk copy to the DB.
|
||||
var dt = new DataTable();
|
||||
dt.Columns.Add("DeviceId");
|
||||
|
||||
foreach (var deviceId in deviceIds)
|
||||
{
|
||||
var row = dt.NewRow();
|
||||
row["DeviceId"] = deviceId;
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand(string.Empty, connection);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Create a target temp table.
|
||||
command.CommandText = "CREATE TABLE #CacheEntriesToRenewTmpTable(DeviceId VARCHAR(255) NOT NULL PRIMARY KEY)";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
|
||||
// Bulk copy the device Ids to renew to the temp table, 1000 records at a time.
|
||||
using SqlBulkCopy bulkcopy = new SqlBulkCopy(connection);
|
||||
bulkcopy.BulkCopyTimeout = BulkCopyBatchTimeout;
|
||||
bulkcopy.BatchSize = BulkCopyBatchSize;
|
||||
bulkcopy.DestinationTableName = "#CacheEntriesToRenewTmpTable";
|
||||
await bulkcopy.WriteToServerAsync(dt);
|
||||
|
||||
// Renew the Hub cache timestamp for every device Id in the temp table.
|
||||
command.CommandTimeout = 300; // The operation should take no longer than 5 minutes
|
||||
command.CommandText = @"UPDATE HubCache SET RenewedAt = GETUTCDATE()
|
||||
FROM HubCache
|
||||
INNER JOIN #CacheEntriesToRenewTmpTable Temp ON (Temp.DeviceId = HubCache.DeviceId)
|
||||
DROP TABLE #CacheEntriesToRenewTmpTable";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a Hub cache entry for a device.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <param name="deviceId">Id of the device for the new cache entry.</param>
|
||||
/// <param name="hub">Hub to be added to the cache entry for the device.</param>
|
||||
public async Task AddOrUpdateHubCacheEntry(Logger logger, string deviceId, string hub)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Adding or updating Hub cache entry for device {deviceId} ({hub})", deviceId, hub);
|
||||
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand("upsertHubCacheEntry", connection)
|
||||
{
|
||||
CommandType = CommandType.StoredProcedure,
|
||||
};
|
||||
|
||||
command.Parameters.Add(new SqlParameter("@DeviceId", deviceId));
|
||||
command.Parameters.Add(new SqlParameter("@Hub", hub));
|
||||
|
||||
await connection.OpenAsync();
|
||||
await command.ExecuteNonQueryAsync();
|
||||
logger.Info("Added or updated Hub cache entry for device {deviceId}", deviceId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all entries in the Hub cache.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to be used.</param>
|
||||
/// <returns>List of all entries in the DB hub cache.</returns>
|
||||
public async Task<List<HubCacheEntry>> ListHubCacheEntries(Logger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Getting all entries in the hub cache");
|
||||
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
var allEntries = new List<HubCacheEntry>();
|
||||
int lastPageSize;
|
||||
int pageIndex = 0;
|
||||
|
||||
do
|
||||
{
|
||||
logger.Info("Fetching page {pageIndex} of Hub cache entries", pageIndex);
|
||||
|
||||
using SqlCommand command = new SqlCommand("getHubCacheEntriesPaged", connection)
|
||||
{
|
||||
CommandType = CommandType.StoredProcedure,
|
||||
};
|
||||
|
||||
command.Parameters.Add(new SqlParameter("@PageIndex", pageIndex++));
|
||||
command.Parameters.Add(new SqlParameter("@RowsPerPage", DefaultPageSIze));
|
||||
|
||||
using SqlDataReader reader = await command.ExecuteReaderAsync();
|
||||
var itemCountBeforePage = allEntries.Count;
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
allEntries.Add(new HubCacheEntry()
|
||||
{
|
||||
DeviceId = reader["DeviceId"].ToString(),
|
||||
Hub = reader["Hub"].ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
lastPageSize = allEntries.Count - itemCountBeforePage;
|
||||
}
|
||||
while (lastPageSize > 0);
|
||||
|
||||
logger.Info("Found {hubCacheEntriesCount} Hub cache entries", allEntries.Count);
|
||||
return allEntries;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an arbitrary SQL command against the DB.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance to use.</param>
|
||||
/// <param name="sql">SQL command to run.</param>
|
||||
public async Task Exec(Logger logger, string sql)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("Executing SQL command");
|
||||
using SqlConnection connection = new SqlConnection(_connectionString);
|
||||
using SqlCommand command = new SqlCommand(sql, connection);
|
||||
await connection.OpenAsync();
|
||||
await command.ExecuteNonQueryAsync();
|
||||
logger.Info("SQL command executed successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw TranslateSqlException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates SQL exceptions into service exceptions.
|
||||
/// </summary>
|
||||
/// <param name="e">Original SQL exception.</param>
|
||||
/// <returns>The translated service exception.</returns>
|
||||
private static BridgeException TranslateSqlException(Exception e)
|
||||
{
|
||||
if (e is SqlException sqlException && (sqlException.Number == StoredProcedureNotFoundErrorNumber || sqlException.Number == TableNotFoundErrorNumber))
|
||||
{
|
||||
return new StorageSetupIncompleteException(e);
|
||||
}
|
||||
|
||||
return new UnknownStorageException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
public class BridgeService : IBridgeService
|
||||
{
|
||||
private readonly ConnectionManager _connectionManager;
|
||||
|
||||
public BridgeService(ConnectionManager connectionManager)
|
||||
{
|
||||
_connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public async Task SendTelemetry(Logger logger, string deviceId, IDictionary<string, object> payload, CancellationToken cancellationToken, IDictionary<string, string> properties = null, string componentName = null, DateTime? creationTimeUtc = null)
|
||||
{
|
||||
logger.Info("Sending telemetry for device {deviceId}", deviceId);
|
||||
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, true /* temporary */, false, cancellationToken);
|
||||
await _connectionManager.SendEventAsync(logger, deviceId, payload, cancellationToken, properties, componentName, creationTimeUtc);
|
||||
}
|
||||
|
||||
public async Task<Twin> GetTwin(Logger logger, string deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.Info("Getting twin for device {deviceId}", deviceId);
|
||||
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, true /* temporary */, false, cancellationToken);
|
||||
return await _connectionManager.GetTwinAsync(logger, deviceId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpdateReportedProperties(Logger logger, string deviceId, IDictionary<string, object> patch, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.Info("Updating reported properties for device {deviceId}", deviceId);
|
||||
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, true /* temporary */, false, cancellationToken);
|
||||
await _connectionManager.UpdateReportedPropertiesAsync(logger, deviceId, patch, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Azure.Devices.Client;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends the default SDK retry policy (ExponentialBackoff) to fail right away if the hub doesn't exist.
|
||||
/// </summary>
|
||||
public class CustomDeviceClientRetryPolicy : IRetryPolicy
|
||||
{
|
||||
private readonly ExponentialBackoff baseRetryPolicy;
|
||||
|
||||
public CustomDeviceClientRetryPolicy()
|
||||
{
|
||||
// Default retry setup of the device SDK.
|
||||
this.baseRetryPolicy = new ExponentialBackoff(int.MaxValue, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if, based on the parameters, the operation should be retried.
|
||||
/// </summary>
|
||||
/// <param name="currentRetryCount">How many times the operation has been retried.</param>
|
||||
/// <param name="lastException">Operation exception.</param>
|
||||
/// <param name="retryInterval">Next retry should be performed after this time interval.</param>
|
||||
/// <returns>True if the operation should be retried, false otherwise.</returns>
|
||||
public bool ShouldRetry(int currentRetryCount, Exception lastException, out TimeSpan retryInterval)
|
||||
{
|
||||
if ((lastException.InnerException as SocketException)?.SocketErrorCode == SocketError.HostNotFound)
|
||||
{
|
||||
retryInterval = TimeSpan.FromMilliseconds(1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// This seems weird, but overriding methods in .net only works when the parent is labeled as virtual. So we cant call base.ShouldRetry and just extend ExponentialBackoff.
|
||||
return baseRetryPolicy.ShouldRetry(currentRetryCount, lastException, out retryInterval);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Common.Exceptions;
|
||||
using DeviceBridge.Providers;
|
||||
using Microsoft.Azure.KeyVault.Models;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
public class EncryptionService
|
||||
{
|
||||
private readonly ISecretsProvider _secretsProvider;
|
||||
private IDictionary<string, SecretBundle> _encryptionKeys;
|
||||
private string _latestKnownEncryptionKeyVersionId = null;
|
||||
|
||||
public EncryptionService(Logger logger, ISecretsProvider secretsProvider)
|
||||
{
|
||||
_secretsProvider = secretsProvider;
|
||||
|
||||
// Initialize secret cache
|
||||
_encryptionKeys = _secretsProvider.GetEncryptionKeyVersions(logger).Result;
|
||||
}
|
||||
|
||||
public async Task<string> Encrypt(Logger logger, string unencryptedString)
|
||||
{
|
||||
var keySecret = await GetEncryptionKey(logger);
|
||||
var encryptionKey = keySecret.Value;
|
||||
|
||||
return $"{keySecret.SecretIdentifier.Version}-{EncryptString(unencryptedString, encryptionKey)}";
|
||||
}
|
||||
|
||||
public async Task<string> Decrypt(Logger logger, string encryptedStringWithVersion)
|
||||
{
|
||||
var encryptedStringParts = encryptedStringWithVersion.Split('-');
|
||||
string keyVersion = null;
|
||||
|
||||
if (encryptedStringParts.Length < 2)
|
||||
{
|
||||
throw new EncryptionException();
|
||||
}
|
||||
|
||||
keyVersion = encryptedStringParts[0];
|
||||
var encryptedString = encryptedStringParts[1];
|
||||
|
||||
var keySecret = await GetEncryptionKey(logger, keyVersion);
|
||||
var encryptionKey = keySecret.Value;
|
||||
|
||||
return DecryptString(encryptedString, encryptionKey);
|
||||
}
|
||||
|
||||
private static string EncryptString(string plainText, string stringKey)
|
||||
{
|
||||
var key = Encoding.ASCII.GetBytes(stringKey);
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = key;
|
||||
aes.GenerateIV();
|
||||
|
||||
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
|
||||
using var streamWriter = new StreamWriter(cryptoStream);
|
||||
|
||||
streamWriter.Write(plainText);
|
||||
streamWriter.Dispose();
|
||||
|
||||
return $"{Convert.ToBase64String(aes.IV)}:{Convert.ToBase64String(memoryStream.ToArray())}";
|
||||
}
|
||||
|
||||
private static string DecryptString(string encryptedStringWithIv, string stringKey)
|
||||
{
|
||||
var key = Encoding.ASCII.GetBytes(stringKey);
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = key;
|
||||
|
||||
var encryptedStringWithIvParts = encryptedStringWithIv.Split(':');
|
||||
var iv = System.Convert.FromBase64String(encryptedStringWithIvParts[0]);
|
||||
aes.IV = iv;
|
||||
|
||||
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||
|
||||
using var memoryStream = new MemoryStream(Convert.FromBase64String(encryptedStringWithIvParts[1]));
|
||||
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
|
||||
using var streamReader = new StreamReader(cryptoStream);
|
||||
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
|
||||
private async Task<SecretBundle> GetEncryptionKey(Logger logger, string version = null)
|
||||
{
|
||||
if (version == null && _latestKnownEncryptionKeyVersionId != null && _encryptionKeys.ContainsKey(_latestKnownEncryptionKeyVersionId))
|
||||
{
|
||||
// Use latest cached version
|
||||
SecretBundle cachedValue;
|
||||
_encryptionKeys.TryGetValue(_latestKnownEncryptionKeyVersionId, out cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
if (version != null && _encryptionKeys.ContainsKey(version))
|
||||
{
|
||||
// Used cached key
|
||||
SecretBundle cachedValue;
|
||||
_encryptionKeys.TryGetValue(version, out cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// Get latest version from KV and cache
|
||||
var foundKey = await _secretsProvider.GetEncryptionKey(logger, version);
|
||||
|
||||
if (!_encryptionKeys.ContainsKey(foundKey.SecretIdentifier.Version))
|
||||
{
|
||||
_encryptionKeys.Add(foundKey.SecretIdentifier.Version, foundKey);
|
||||
}
|
||||
|
||||
if (version == null)
|
||||
{
|
||||
_latestKnownEncryptionKeyVersionId = foundKey.SecretIdentifier.Version;
|
||||
}
|
||||
|
||||
return foundKey;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// When the application starts, start the expired connection cleanup task.
|
||||
/// </summary>
|
||||
public class ExpiredConnectionCleanupHostedService : IHostedService
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly ConnectionManager _connectionManager;
|
||||
|
||||
public ExpiredConnectionCleanupHostedService(Logger logger, ConnectionManager connectionManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var _ = _connectionManager.StartExpiredConnectionCleanupAsync().ContinueWith(t => _logger.Error(t.Exception, "Failed to start expired connection cleanup task"), TaskContinuationOptions.OnlyOnFaulted);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Providers;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Every 6 hours:
|
||||
/// - renews the Hub cache entries for the devices that attempted to open a connection.
|
||||
/// - runs the GC routine in the Hub cache, removing entries for any device that doesn't have a subscription and
|
||||
/// hasn't connected in the last week.
|
||||
/// </summary>
|
||||
public class HubCacheGcHostedService : IHostedService, IDisposable
|
||||
{
|
||||
private const double HubCacheGcIntervalHours = 6; // How often to run the Hub cache GC task
|
||||
|
||||
private readonly Logger _logger;
|
||||
private readonly IStorageProvider _storageProvider;
|
||||
private readonly ConnectionManager _connectionManager;
|
||||
private Timer _timer;
|
||||
|
||||
public HubCacheGcHostedService(Logger logger, IStorageProvider storageProvider, ConnectionManager connectionManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageProvider = storageProvider;
|
||||
_connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.Info("Initializing Hub cache GC hosted service");
|
||||
_timer = new Timer(Run, null, TimeSpan.FromHours(HubCacheGcIntervalHours), TimeSpan.FromHours(HubCacheGcIntervalHours));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.Info("Hub cache GC hosted service is stopping.");
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private void Run(object state)
|
||||
{
|
||||
var _ = RunAsync().ContinueWith(t => _logger.Error(t.Exception, "Failed to run Hub cache GC"), TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
private async Task RunAsync()
|
||||
{
|
||||
// Get all devices that connected since the last period + 30min.
|
||||
var devicesThatConnectedSinceLastRun = _connectionManager.GetDevicesThatConnectedSince(DateTime.Now.Subtract(TimeSpan.FromHours(HubCacheGcIntervalHours)).Subtract(TimeSpan.FromMinutes(30)));
|
||||
await _storageProvider.RenewHubCacheEntries(_logger, devicesThatConnectedSinceLastRun);
|
||||
await _storageProvider.GcHubCache(_logger);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
public interface IBridgeService
|
||||
{
|
||||
Task<Twin> GetTwin(Logger logger, string deviceId, CancellationToken cancellationToken);
|
||||
|
||||
Task SendTelemetry(Logger logger, string deviceId, IDictionary<string, object> payload, CancellationToken cancellationToken, IDictionary<string, string> properties = null, string componentName = null, DateTime? creationTimeUtc = null);
|
||||
|
||||
Task UpdateReportedProperties(Logger logger, string deviceId, IDictionary<string, object> patch, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,486 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Providers;
|
||||
using Microsoft.Azure.Devices.Client;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
public class SubscriptionService
|
||||
{
|
||||
public const uint DefaultRampupBatchSize = 150; // How many devices to synchronize at a time when performing a full DB sync of all subscriptions
|
||||
public const uint DefaultRampupBatchIntervalMs = 1000; // How long to wait between each batch when performing a full DB sync of all subscriptions
|
||||
|
||||
private const string SubscriptionStatusStarting = "Starting";
|
||||
private const string SubscriptionStatusRunning = "Running";
|
||||
private const string SubscriptionStatusStopped = "Stopped";
|
||||
|
||||
private readonly uint _rampupBatchSize;
|
||||
private readonly uint _rampupBatchIntervalMs;
|
||||
private readonly IStorageProvider _storageProvider;
|
||||
private readonly ConnectionManager _connectionManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly Logger _logger;
|
||||
private readonly ConcurrentDictionary<string, List<DeviceSubscription>> dataSubscriptionsToInitialize;
|
||||
private ConcurrentDictionary<string, SemaphoreSlim> _dbAndConnectionStateSyncSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
private ConcurrentDictionary<string, SemaphoreSlim> _connectionStatusSubscriptionSyncSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
public SubscriptionService(Logger logger, ConnectionManager connectionManager, IStorageProvider storageProvider, IHttpClientFactory httpClientFactory, uint rampupBatchSize, uint rampupBatchIntervalMs)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageProvider = storageProvider;
|
||||
_connectionManager = connectionManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_rampupBatchSize = rampupBatchSize;
|
||||
_rampupBatchIntervalMs = rampupBatchIntervalMs;
|
||||
|
||||
// Fetch from DB all subscriptions to be initialized on service startup. This must be done synchronously before
|
||||
// the service instance is fully constructed, so subsequent requests received by the service can prevent a device
|
||||
// from being initialized with stale data by removing items from this list.
|
||||
_logger.Info("Attempting to fetch all subscriptions to initialize from DB");
|
||||
var allSubscriptions = _storageProvider.ListAllSubscriptionsOrderedByDeviceId(_logger).Result;
|
||||
dataSubscriptionsToInitialize = new ConcurrentDictionary<string, List<DeviceSubscription>>();
|
||||
string currentDeviceId = null;
|
||||
List<DeviceSubscription> currentDeviceDataSubscriptions = null;
|
||||
|
||||
foreach (var subscription in allSubscriptions.FindAll(s => s.SubscriptionType.IsDataSubscription()))
|
||||
{
|
||||
var deviceId = subscription.DeviceId;
|
||||
|
||||
// Since results are grouped by device Id, we store the results of a device once we move to the next one.
|
||||
if (deviceId != currentDeviceId)
|
||||
{
|
||||
if (currentDeviceId != null && currentDeviceDataSubscriptions != null)
|
||||
{
|
||||
dataSubscriptionsToInitialize.TryAdd(currentDeviceId, currentDeviceDataSubscriptions);
|
||||
}
|
||||
|
||||
currentDeviceId = deviceId;
|
||||
currentDeviceDataSubscriptions = new List<DeviceSubscription>();
|
||||
}
|
||||
|
||||
currentDeviceDataSubscriptions.Add(subscription);
|
||||
}
|
||||
|
||||
// Store the results for the last device.
|
||||
if (currentDeviceId != null && currentDeviceDataSubscriptions != null)
|
||||
{
|
||||
dataSubscriptionsToInitialize.TryAdd(currentDeviceId, currentDeviceDataSubscriptions);
|
||||
}
|
||||
|
||||
// Synchronously initialize all connection status subscriptions before the service is fully constructed. This ensures that
|
||||
// subscriptions are in place before any connection can be established, so no status change events are missed.
|
||||
// No lock is needed, since no other concurrent operation is received until the service starts.
|
||||
_logger.Info("Attempting to initialize all connection status subscriptions");
|
||||
|
||||
foreach (var connectionStatusSubscription in allSubscriptions.FindAll(s => s.SubscriptionType == DeviceSubscriptionType.ConnectionStatus))
|
||||
{
|
||||
_connectionManager.SetConnectionStatusCallback(connectionStatusSubscription.DeviceId, GetConnectionStatusChangeCallback(connectionStatusSubscription.DeviceId, connectionStatusSubscription));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DeviceSubscription> GetConnectionStatusSubscription(Logger logger, string deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _storageProvider.GetDeviceSubscription(logger, deviceId, DeviceSubscriptionType.ConnectionStatus, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<DeviceSubscription> CreateOrUpdateConnectionStatusSubscription(Logger logger, string deviceId, string callbackUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
// We need to synchronize the DB and callback update to make sure that the registered callback always reflects the actual data in the DB.
|
||||
// We use a different mutex from data subscriptions as they might take a long time to synchronize due to connection creation.
|
||||
var mutex = _connectionStatusSubscriptionSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
|
||||
await mutex.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Info("Acquired connection status subscription sync lock for device {deviceId}", deviceId);
|
||||
var subscription = await _storageProvider.CreateOrUpdateDeviceSubscription(logger, deviceId, DeviceSubscriptionType.ConnectionStatus, callbackUrl, cancellationToken);
|
||||
_connectionManager.SetConnectionStatusCallback(deviceId, GetConnectionStatusChangeCallback(deviceId, subscription));
|
||||
return subscription;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteConnectionStatusSubscription(Logger logger, string deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
// We need to synchronize the DB and callback update to make sure that the registered callback always reflects the actual data in the DB.
|
||||
// We use a different mutex from data subscriptions as they might take a long time to synchronize due to connection creation.
|
||||
var mutex = _connectionStatusSubscriptionSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
|
||||
await mutex.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Info("Acquired connection status subscription sync lock for device {deviceId}", deviceId);
|
||||
await _storageProvider.DeleteDeviceSubscription(logger, deviceId, DeviceSubscriptionType.ConnectionStatus, cancellationToken);
|
||||
_connectionManager.RemoveConnectionStatusCallback(deviceId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DeviceSubscriptionWithStatus> GetDataSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
|
||||
{
|
||||
var subscription = await _storageProvider.GetDeviceSubscription(logger, deviceId, subscriptionType, cancellationToken);
|
||||
|
||||
return (subscription != null) ? new DeviceSubscriptionWithStatus(subscription)
|
||||
{
|
||||
Status = ComputeDataSubscriptionStatus(deviceId, subscription.SubscriptionType, subscription.CallbackUrl),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task<DeviceSubscriptionWithStatus> CreateOrUpdateDataSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
var subscription = await _storageProvider.CreateOrUpdateDeviceSubscription(logger, deviceId, subscriptionType, callbackUrl, cancellationToken);
|
||||
var _ = SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId).ContinueWith(t => _logger.Error(t.Exception, "Failed to synchronize DB subscriptions and connection state for device {deviceId}", deviceId), TaskContinuationOptions.OnlyOnFaulted);
|
||||
return new DeviceSubscriptionWithStatus(subscription)
|
||||
{
|
||||
Status = ComputeDataSubscriptionStatus(deviceId, subscription.SubscriptionType, subscription.CallbackUrl),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteDataSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
|
||||
{
|
||||
await _storageProvider.DeleteDeviceSubscription(logger, deviceId, subscriptionType, cancellationToken);
|
||||
var _ = SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId).ContinueWith(t => _logger.Error(t.Exception, "Failed to synchronize DB subscriptions and connection state for device {deviceId}", deviceId), TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the Initialization of data subscriptions for all devices based on the list fetched from the DB at service construction time.
|
||||
/// For use during service startup.
|
||||
/// </summary>
|
||||
public async Task StartDataSubscriptionsInitializationAsync()
|
||||
{
|
||||
_logger.Info("Attempting to initialize subscriptions for all devices");
|
||||
var deviceIds = dataSubscriptionsToInitialize.Keys.ToList();
|
||||
|
||||
// Synchronizes one batch of devices at a time.
|
||||
for (int i = 0; i < deviceIds.Count; ++i)
|
||||
{
|
||||
var deviceId = deviceIds[i];
|
||||
var _ = SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId, true).ContinueWith(t => _logger.Error(t.Exception, "Failed to initialize DB subscriptions for device {deviceId}", deviceId), TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
if ((i + 1) % _rampupBatchSize == 0 && (i + 1) < deviceIds.Count)
|
||||
{
|
||||
_logger.Info("Waiting {subscriptionFullSyncBatchIntervalMs} ms before syncing subscriptions for next {subscriptionFullSyncBatchSize} devices", _rampupBatchIntervalMs, _rampupBatchSize);
|
||||
await Task.Delay((int)_rampupBatchIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Successfully initialized subscriptions from DB for {deviceCount} devices", deviceIds.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that the internal engine state (connection and callbacks) for a device reflects the subscriptions status and callbacks stored in the DB or in the initialization list.
|
||||
/// Only applies to data subscriptions, i.e., not connection status subscriptions.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">Id of the device to synchronize subscriptions for.</param>
|
||||
/// <param name="useInitializationList">Whether subscriptions should be pulled from the initialization list or fetched from the DB.</param>
|
||||
/// <param name="forceConnectionRetry">Whether to force a connection retry if the current connection is in a failed state.</param>
|
||||
public async Task SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(string deviceId, bool useInitializationList = false, bool forceConnectionRetry = false)
|
||||
{
|
||||
_logger.Info("Attempting to synchronize DB and engine subscriptions for device {deviceId}", deviceId);
|
||||
|
||||
// Synchronizing this code is the only way to guarantee that the current state of the runner will always reflect the latest state in the DB.
|
||||
// Otherwise we could end up in an inconsistent state if a subscription is deleted and recreated too fast or if a subscription is modified
|
||||
// while we're initializing the device with data fetched from the DB on startup.
|
||||
var mutex = _dbAndConnectionStateSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
|
||||
await mutex.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Info("Acquired DB and connection state sync lock for device {deviceId}", deviceId);
|
||||
List<DeviceSubscription> dataSubscriptions;
|
||||
|
||||
if (useInitializationList)
|
||||
{
|
||||
if (!dataSubscriptionsToInitialize.TryGetValue(deviceId, out dataSubscriptions))
|
||||
{
|
||||
_logger.Info("Subscriptions for Device {deviceId} not found in initialization list", deviceId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dataSubscriptions = (await _storageProvider.ListDeviceSubscriptions(_logger, deviceId)).FindAll(s => s.SubscriptionType.IsDataSubscription());
|
||||
}
|
||||
|
||||
// Remove the device form the initialization list to mark it as already initialized.
|
||||
dataSubscriptionsToInitialize.TryRemove(deviceId, out _);
|
||||
|
||||
// If a desired property subscription exists, register the callback. If not, ensure that the callback doesn't exist.
|
||||
var desiredPropertySubscription = dataSubscriptions.Find(s => s.SubscriptionType == DeviceSubscriptionType.DesiredProperties);
|
||||
if (desiredPropertySubscription != null)
|
||||
{
|
||||
await _connectionManager.SetDesiredPropertyUpdateCallbackAsync(deviceId, desiredPropertySubscription.CallbackUrl, GetDesiredPropertyUpdateCallback(deviceId, desiredPropertySubscription));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _connectionManager.RemoveDesiredPropertyUpdateCallbackAsync(deviceId);
|
||||
}
|
||||
|
||||
// If a method subscription exists, register the callback. If not, ensure that the callback doesn't exist.
|
||||
var methodSubscription = dataSubscriptions.Find(s => s.SubscriptionType == DeviceSubscriptionType.Methods);
|
||||
if (methodSubscription != null)
|
||||
{
|
||||
await _connectionManager.SetMethodCallbackAsync(deviceId, methodSubscription.CallbackUrl, GetMethodCallback(deviceId, methodSubscription));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _connectionManager.RemoveMethodCallbackAsync(deviceId);
|
||||
}
|
||||
|
||||
// If a C2D subscription exists, register the callback. If not, ensure that the callback doesn't exist.
|
||||
var messageSubscription = dataSubscriptions.Find(s => s.SubscriptionType == DeviceSubscriptionType.C2DMessages);
|
||||
if (messageSubscription != null)
|
||||
{
|
||||
await _connectionManager.SetMessageCallbackAsync(deviceId, messageSubscription.CallbackUrl, GetReceiveC2DMessageCallback(deviceId, messageSubscription));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _connectionManager.RemoveMessageCallbackAsync(deviceId);
|
||||
}
|
||||
|
||||
// The device needs a connection constantly open if at least one data subscription exists. If not, the connection can be closed.
|
||||
if (dataSubscriptions.Count > 0)
|
||||
{
|
||||
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, false /* permanent */, forceConnectionRetry);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _connectionManager.AssertDeviceConnectionClosedAsync(deviceId);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the status of a subscription based on the current state of the device client.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">Id of the device for which to check the subscription status.</param>
|
||||
/// <param name="subscriptionType">Type of the subscription that we want the status for.</param>
|
||||
/// <param name="callbackUrl">URL for which we want to check the subscription status.</param>
|
||||
/// <returns>Status of the subscription.</returns>
|
||||
private string ComputeDataSubscriptionStatus(string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl)
|
||||
{
|
||||
// If the callback URL in storage does not match the one currently registered in the client, we can assume that the engine
|
||||
// is still trying to synchronize this subscription (either it was just created or it's being initialized at startup).
|
||||
if ((subscriptionType == DeviceSubscriptionType.DesiredProperties && _connectionManager.GetCurrentDesiredPropertyUpdateCallbackId(deviceId) != callbackUrl) ||
|
||||
(subscriptionType == DeviceSubscriptionType.Methods && _connectionManager.GetCurrentMethodCallbackId(deviceId) != callbackUrl) ||
|
||||
(subscriptionType == DeviceSubscriptionType.C2DMessages && _connectionManager.GetCurrentMessageCallbackId(deviceId) != callbackUrl))
|
||||
{
|
||||
return SubscriptionStatusStarting;
|
||||
}
|
||||
|
||||
var deviceStatus = _connectionManager.GetDeviceStatus(deviceId);
|
||||
|
||||
if (deviceStatus?.status == ConnectionStatus.Connected)
|
||||
{
|
||||
// Device is connected and callback is registered, so the subscription is running.
|
||||
return SubscriptionStatusRunning;
|
||||
}
|
||||
else if (deviceStatus?.status == ConnectionStatus.Disconnected || deviceStatus?.status == ConnectionStatus.Disabled)
|
||||
{
|
||||
// Callbacks match, but the device is disconnected and the SDK won't automatically retry, so the subscription is permanently stopped.
|
||||
return SubscriptionStatusStopped;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the device is not explicitly connected or disconnected, we can assume that the SDK is retrying or a client is being created.
|
||||
return SubscriptionStatusStarting;
|
||||
}
|
||||
}
|
||||
|
||||
private DesiredPropertyUpdateCallback GetDesiredPropertyUpdateCallback(string deviceId, DeviceSubscription desiredPropertySubscription)
|
||||
{
|
||||
return async (desiredPopertyUpdate, _) =>
|
||||
{
|
||||
_logger.Info("Got desired property update for device {deviceId}. Callback URL: {callbackUrl}. Payload: {desiredPopertyUpdate}", deviceId, desiredPropertySubscription.CallbackUrl, desiredPopertyUpdate.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
var body = new DesiredPropertyUpdateEventBody()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceReceivedAt = DateTime.UtcNow,
|
||||
DesiredProperties = new JRaw(desiredPopertyUpdate.ToJson()),
|
||||
};
|
||||
|
||||
var payload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(desiredPropertySubscription.CallbackUrl, payload);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
_logger.Info("Successfully executed desired property update callback for device {deviceId}. Callback status code {statusCode}", deviceId, httpResponse.StatusCode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to execute desired property update callback for device {deviceId}", deviceId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Func<Message, Task<ReceiveMessageCallbackStatus>> GetReceiveC2DMessageCallback(string deviceId, DeviceSubscription messageSubscription)
|
||||
{
|
||||
_logger.Info("Creating C2D callback {deviceId}. Callback URL {callbackUrl}", deviceId, messageSubscription.CallbackUrl);
|
||||
return async (receivedMessage) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using StreamReader reader = new StreamReader(receivedMessage.BodyStream);
|
||||
var messageBody = reader.ReadToEnd();
|
||||
_logger.Info("Got C2D message for device {deviceId}. Callback URL {callbackUrl}. Payload: {payload}", deviceId, messageSubscription.CallbackUrl, messageBody);
|
||||
|
||||
var body = new C2DMessageInvocationEventBody()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceReceivedAt = DateTime.UtcNow,
|
||||
MessageBody = new JRaw(messageBody),
|
||||
Properties = receivedMessage.Properties,
|
||||
MessageId = receivedMessage.MessageId,
|
||||
ExpiryTimeUTC = receivedMessage.ExpiryTimeUtc,
|
||||
};
|
||||
|
||||
// Send request to callback URL
|
||||
var requestPayload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(messageSubscription.CallbackUrl, requestPayload);
|
||||
var statusCode = (int)httpResponse.StatusCode;
|
||||
if (statusCode >= 200 && statusCode < 300)
|
||||
{
|
||||
_logger.Info("Received C2D message callback with status {statusCode}, request accepted.", statusCode);
|
||||
return ReceiveMessageCallbackStatus.Accept;
|
||||
}
|
||||
|
||||
if (statusCode >= 400 && statusCode < 500)
|
||||
{
|
||||
_logger.Info("Received C2D message callback with status {statusCode}, request rejected.", statusCode);
|
||||
return ReceiveMessageCallbackStatus.Reject;
|
||||
}
|
||||
|
||||
_logger.Info("Received C2D message callback with status {statusCode}, request abandoned.", statusCode);
|
||||
return ReceiveMessageCallbackStatus.Abandon;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to execute message callback, device {deviceId}. Request abandoned.", deviceId);
|
||||
return ReceiveMessageCallbackStatus.Abandon;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private MethodCallback GetMethodCallback(string deviceId, DeviceSubscription methodSubscription)
|
||||
{
|
||||
return async (methodRequest, _) =>
|
||||
{
|
||||
_logger.Info("Got method request for device {deviceId}. Callback URL {callbackUrl}. Method: {methodName}. Payload: {payload}", deviceId, methodSubscription.CallbackUrl, methodRequest.Name, methodRequest.DataAsJson);
|
||||
|
||||
try
|
||||
{
|
||||
var body = new MethodInvocationEventBody()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceReceivedAt = DateTime.UtcNow,
|
||||
MethodName = methodRequest.Name,
|
||||
RequestData = new JRaw(methodRequest.DataAsJson),
|
||||
};
|
||||
|
||||
// Send request to callback URL
|
||||
var requestPayload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(methodSubscription.CallbackUrl, requestPayload);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
|
||||
// Read method response from callback response
|
||||
using var responseStream = await httpResponse.Content.ReadAsStreamAsync();
|
||||
MethodResponseBody responseBody = null;
|
||||
|
||||
try
|
||||
{
|
||||
responseBody = await System.Text.Json.JsonSerializer.DeserializeAsync<MethodResponseBody>(responseStream, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
}
|
||||
catch (System.Text.Json.JsonException e)
|
||||
{
|
||||
_logger.Error(e, "Received malformed JSON response when executing method callback for device {deviceId}", deviceId);
|
||||
}
|
||||
|
||||
MethodResponse methodResponse;
|
||||
string serializedResponsePayload = null;
|
||||
int status = 200;
|
||||
|
||||
// If we got a custom response, return the custom payload and status. If not, just respond with a 200.
|
||||
if (responseBody != null && responseBody.Status != null)
|
||||
{
|
||||
status = responseBody.Status.Value;
|
||||
}
|
||||
|
||||
if (responseBody != null && responseBody.Payload != null)
|
||||
{
|
||||
serializedResponsePayload = System.Text.Json.JsonSerializer.Serialize(responseBody.Payload);
|
||||
methodResponse = new MethodResponse(Encoding.UTF8.GetBytes(serializedResponsePayload), status);
|
||||
}
|
||||
else
|
||||
{
|
||||
methodResponse = new MethodResponse(status);
|
||||
}
|
||||
|
||||
_logger.Info("Successfully executed method callback for device {deviceId}. Response status: {responseStatus}. Response payload: {responsePayload}", deviceId, status, serializedResponsePayload);
|
||||
return methodResponse;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to execute method callback for device {deviceId}", deviceId);
|
||||
return new MethodResponse(500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Func<ConnectionStatus, ConnectionStatusChangeReason, Task> GetConnectionStatusChangeCallback(string deviceId, DeviceSubscription connectionStatusSubscription)
|
||||
{
|
||||
return async (status, reason) =>
|
||||
{
|
||||
_logger.Info("Got connection status change for device {deviceId}. Callback URL: {callbackUrl}. Status: {status}. Reason: {reason}", deviceId, connectionStatusSubscription.CallbackUrl, status, reason);
|
||||
|
||||
try
|
||||
{
|
||||
var body = new ConnectionStatusChangeEventBody()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceReceivedAt = DateTime.UtcNow,
|
||||
Status = status.ToString(),
|
||||
Reason = reason.ToString(),
|
||||
};
|
||||
|
||||
var payload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
|
||||
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(connectionStatusSubscription.CallbackUrl, payload);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
_logger.Info("Successfully executed connection status change callback for device {deviceId}. Callback status code {statusCode}", deviceId, httpResponse.StatusCode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to execute connection status change callback for device {deviceId}", deviceId);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NLog;
|
||||
|
||||
namespace DeviceBridge.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// When the application starts, initialize all device subscriptions that we have in the DB.
|
||||
/// </summary>
|
||||
public class SubscriptionStartupHostedService : IHostedService
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly SubscriptionService _subscriptionService;
|
||||
|
||||
public SubscriptionStartupHostedService(Logger logger, SubscriptionService subscriptionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_subscriptionService = subscriptionService;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var _ = _subscriptionService.StartDataSubscriptionsInitializationAsync().ContinueWith(t => _logger.Error(t.Exception, "Failed to start subscription initialization task"), TaskContinuationOptions.OnlyOnFaulted);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Common;
|
||||
using DeviceBridge.Common.Authentication;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Providers;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NLog;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace DeviceBridge
|
||||
{
|
||||
/// <summary>Class Startup.</summary>
|
||||
public class Startup
|
||||
{
|
||||
private static Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="Startup"/> class.</summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>This method gets called by the runtime. Use this method to add services to the container.</summary>
|
||||
/// <param name="services">The services.</param>
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
_logger.Info("Configuring services");
|
||||
|
||||
string kvUrl = Environment.GetEnvironmentVariable("KV_URL");
|
||||
|
||||
// Build cache from Key Vault
|
||||
var secretsService = new SecretsProvider(kvUrl);
|
||||
var idScope = secretsService.GetIdScopeAsync(_logger).Result;
|
||||
var sasKey = secretsService.GetIotcSasKeyAsync(_logger).Result;
|
||||
var sqlConnectionString = Utils.GetSqlConnectionString(_logger, secretsService);
|
||||
|
||||
// Override defaults
|
||||
var customMaxPoolSize = Environment.GetEnvironmentVariable("MAX_POOL_SIZE");
|
||||
var customRampupBatchSize = Environment.GetEnvironmentVariable("DEVICE_RAMPUP_BATCH_SIZE");
|
||||
var customRampupBatchIntervalMs = Environment.GetEnvironmentVariable("DEVICE_RAMPUP_BATCH_INTERVAL_MS");
|
||||
uint maxPoolSize = (customMaxPoolSize != null && customMaxPoolSize != string.Empty) ? Convert.ToUInt32(customMaxPoolSize, 10) : ConnectionManager.DeafultMaxPoolSize;
|
||||
uint rampupBatchSize = (customRampupBatchSize != null && customRampupBatchSize != string.Empty) ? Convert.ToUInt32(customRampupBatchSize, 10) : SubscriptionService.DefaultRampupBatchSize;
|
||||
uint rampupBatchIntervalMs = (customRampupBatchIntervalMs != null && customRampupBatchIntervalMs != string.Empty) ? Convert.ToUInt32(customRampupBatchIntervalMs, 10) : SubscriptionService.DefaultRampupBatchIntervalMs;
|
||||
|
||||
_logger.SetProperty("idScope", idScope);
|
||||
_logger.SetProperty("cv", Guid.NewGuid()); // CV for all background operations
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
// Start services
|
||||
services.AddSingleton<ISecretsProvider>(secretsService);
|
||||
services.AddSingleton(_logger);
|
||||
services.AddSingleton<EncryptionService>();
|
||||
services.AddSingleton<IStorageProvider>(provider => new StorageProvider(sqlConnectionString, provider.GetRequiredService<EncryptionService>()));
|
||||
services.AddSingleton(provider => new ConnectionManager(provider.GetRequiredService<Logger>(), idScope, sasKey, maxPoolSize, provider.GetRequiredService<IStorageProvider>()));
|
||||
services.AddSingleton(provider => new SubscriptionService(provider.GetRequiredService<Logger>(), provider.GetRequiredService<ConnectionManager>(), provider.GetRequiredService<IStorageProvider>(), provider.GetRequiredService<IHttpClientFactory>(), rampupBatchSize, rampupBatchIntervalMs));
|
||||
services.AddSingleton<IBridgeService, BridgeService>();
|
||||
services.AddHttpClient("RetryClient").AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
services.AddHostedService<ExpiredConnectionCleanupHostedService>();
|
||||
services.AddHostedService<SubscriptionStartupHostedService>();
|
||||
services.AddHostedService<HubCacheGcHostedService>();
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultScheme = SchemesNamesConst.TokenAuthenticationDefaultScheme;
|
||||
})
|
||||
.AddScheme<TokenAuthenticationOptions, TokenAuthenticationHandler>(SchemesNamesConst.TokenAuthenticationDefaultScheme, o => { });
|
||||
|
||||
services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add(new AuthorizeFilter());
|
||||
});
|
||||
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
// Set XML comments.
|
||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
options.IncludeXmlComments(xmlPath);
|
||||
|
||||
options.CustomOperationIds(apiDesc => apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
|
||||
|
||||
// Type mappers for custom serialization.
|
||||
options.MapType(typeof(DeviceSubscriptionType), () => DeviceSubscriptionType.Schema);
|
||||
options.MapType(typeof(DeviceTwin), () => DeviceTwin.Schema);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>This method gets called by the runtime. Use this method to configure the HTTP request pipeline..</summary>
|
||||
/// <param name="app">The application.</param>
|
||||
/// <param name="env">The env.</param>
|
||||
/// <param name="lifetime">The lifetime.</param>
|
||||
/// <param name="connectionManager">The connection manager.</param>
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime, ConnectionManager connectionManager)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseSwagger(c =>
|
||||
{
|
||||
c.SerializeAsV2 = true;
|
||||
});
|
||||
|
||||
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets the retry policy, used in HttpClient.</para>
|
||||
/// </summary>
|
||||
/// <returns>IAsyncPolicy<HttpResponseMessage>.</returns>
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
// Handles 5XX, 408 and 429 status codes.
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(msg => msg.StatusCode == (HttpStatusCode)429)
|
||||
.WaitAndRetryAsync(
|
||||
retryCount: Convert.ToInt32(Environment.GetEnvironmentVariable("HTTP_RETRY_LIMIT")),
|
||||
sleepDurationProvider: (retryCount, response, context) =>
|
||||
{
|
||||
// Observe server Retry-After if applicable
|
||||
IEnumerable<string> retryAfterValues;
|
||||
if (response.Result.Headers.TryGetValues("Retry-After", out retryAfterValues))
|
||||
{
|
||||
return TimeSpan.FromSeconds(Convert.ToDouble(retryAfterValues.FirstOrDefault()));
|
||||
}
|
||||
|
||||
return TimeSpan.FromSeconds(Math.Pow(2, retryCount));
|
||||
},
|
||||
onRetryAsync: async (response, timespan, retryCount, context) =>
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"companyName": "Microsoft Corporation",
|
||||
"xmlHeader": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace DeviceBridgeTests
|
||||
{
|
||||
public static class TestConstants
|
||||
{
|
||||
public const string KeyvaultUrl = "";
|
||||
public const string IdScope = "";
|
||||
public const string SasKey = "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridge.Models;
|
||||
using DeviceBridge.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
using NLog;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DeviceBridge.Controllers.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class MessagesControllerTests
|
||||
{
|
||||
private const string MockDeviceId = "test-device";
|
||||
private Mock<IBridgeService> _bridgeServiceMock;
|
||||
private MessagesController _messagesController;
|
||||
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
_bridgeServiceMock = new Mock<IBridgeService>();
|
||||
_messagesController = new MessagesController(LogManager.GetCurrentClassLogger(), _bridgeServiceMock.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("SendTelemetry should convert creation time to UTC, call BridgeService.SendTelemetry, and return a 200 Ok")]
|
||||
public async Task SendMessage()
|
||||
{
|
||||
var mockBody = new MessageBody()
|
||||
{
|
||||
ComponentName = "MyComponent",
|
||||
CreationTimeUtc = DateTime.Parse("02/10/2018 11:25:27 +08:00"),
|
||||
Properties = new Dictionary<string, string>()
|
||||
{
|
||||
{ "prop", "val" },
|
||||
},
|
||||
Data = new Dictionary<string, object>()
|
||||
{
|
||||
{ "temperature", 4 },
|
||||
},
|
||||
};
|
||||
|
||||
var result = await _messagesController.SendMessage(MockDeviceId, mockBody);
|
||||
|
||||
Assert.That(result, Is.InstanceOf<OkResult>());
|
||||
_bridgeServiceMock.Verify(p => p.SendTelemetry(It.IsAny<Logger>(), MockDeviceId, mockBody.Data, default, mockBody.Properties, mockBody.ComponentName, It.Is<DateTime>(d => d == mockBody.CreationTimeUtc && d.Kind == DateTimeKind.Utc)), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DeviceBridge.Controllers.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SessionControllerTests
|
||||
{
|
||||
[Test]
|
||||
public async Task Get()
|
||||
{
|
||||
// TODO
|
||||
// Calls GetDeviceSession with the correct deviceId
|
||||
// Returns 200 if session exists
|
||||
// Returns 404 if session doesn't exist
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateOrUpdate()
|
||||
{
|
||||
// TODO
|
||||
// Sets input expiry to UTC
|
||||
// Calls CreateOrUpdateDeviceSession with correct deviceId and expiry
|
||||
// Calls InitializeDeviceClientAsync with the correct deviceId
|
||||
// Does not call InitializeDeviceClientAsync if CreateOrUpdateDeviceSession fails
|
||||
// Returns 200 and the created/updated session
|
||||
// Returns 400 if CreateOrUpdateDeviceSession throws ExpiresAtLessThanCurrentTimeException
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Delete()
|
||||
{
|
||||
// TODO
|
||||
// Calls DeleteDeviceSession with the correct deviceId
|
||||
// Calls TearDownDeviceClientAsync with correct deviceId, without awaiting the result
|
||||
// Does no call TearDownDeviceClientAsync if DeleteDeviceSession fails
|
||||
// Returns 204
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RootNamespace>DeviceBridgeTests</RootNamespace>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.15.2" />
|
||||
<PackageReference Include="nunit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DeviceBridge\DeviceBridge.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1604:Element documentation should have summary", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1615:Element return value should be documented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:Partial elements should be documented", Justification = "Not all elements must be documented")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Ok to omit this prefix")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Conflicts with VS rule")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Using underscore for private class fields")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "Prevents the use of underscore for unused local variable")]
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using DeviceBridgeTests;
|
||||
using NLog.Web;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DeviceBridge.Providers.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SecretsProviderTests
|
||||
{
|
||||
private const string TmpIdScope = "tmpIdScope";
|
||||
private const string TmpSasKey = "tmpSasKey";
|
||||
|
||||
private NLog.Logger logger;
|
||||
private SecretsProvider sp;
|
||||
|
||||
[SetUp]
|
||||
public async Task Init()
|
||||
{
|
||||
logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
|
||||
sp = new SecretsProvider(TestConstants.KeyvaultUrl);
|
||||
await sp.PutSecretAsync(logger, SecretsProvider.IotcIdScope, TmpIdScope);
|
||||
await sp.PutSecretAsync(logger, SecretsProvider.IotcSasKey, TmpSasKey);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetIdScopeAsyncTest()
|
||||
{
|
||||
var idScope = await sp.GetIdScopeAsync(logger);
|
||||
Assert.AreEqual(TmpIdScope, idScope);
|
||||
// TODO
|
||||
// Calls GetSecretAsync with correct secret name
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetIotcSasKeyAsyncTest()
|
||||
{
|
||||
var sasKey = await sp.GetIotcSasKeyAsync(logger);
|
||||
Assert.AreEqual(TmpSasKey, sasKey);
|
||||
// TODO
|
||||
// Calls GetSecretAsync with correct secret name
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DeviceBridge.Providers.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class StorageProviderTests
|
||||
{
|
||||
[Test]
|
||||
public async Task GetDeviceSession()
|
||||
{
|
||||
// TODO
|
||||
// Passes the right query
|
||||
// Adds deviceId as parameter
|
||||
// Return all session fields, including expiry
|
||||
// Correctly converts field types
|
||||
// Returns null if session isn't found
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateOrUpdateDeviceSession()
|
||||
{
|
||||
// TODO
|
||||
// Calls the right procedure
|
||||
// Adds deviceId and expiry as parameter
|
||||
// Returns updated session, including updatedAt from query output
|
||||
// Throws custom exception if query fails with "ExpiresAt must be greater than current time"
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteDeviceSession()
|
||||
{
|
||||
// TODO
|
||||
// Passes the right query
|
||||
// Adds deviceId as parameter
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DeviceBridge.Services.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class BridgeServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task SendTelemetry()
|
||||
{
|
||||
// TODO
|
||||
// Calls SendEventAsync
|
||||
// Passes correct payload
|
||||
// Passes cancellation token matching HTTP timeout
|
||||
// Fails if client wasn't found
|
||||
// Fails if using an already-disposed client
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DeviceBridge.Services.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ConnectionManagerServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task TryGetDeviceClient()
|
||||
{
|
||||
// TODO
|
||||
// Returns device client if one exists
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InitializeDeviceClientAsync()
|
||||
{
|
||||
// TODO
|
||||
// Multiple calls to InitializeDeviceClientAsync do not run in parallel (test mutual exclusion)
|
||||
// Calls to InitializeDeviceClientAsync and TearDownDeviceClientAsync do not run in parallel (test mutual exclusion)
|
||||
// Returns existing client if one already exists
|
||||
// Tries to connect to cached device hub, if one exists, before attempting other known hubs
|
||||
// Tries to connect to all known hubs before trying DPS registration
|
||||
// Fails right away if OpenAsync throws an exception not in the list of expected errors
|
||||
// Tries DPS registration with correct key if attempts to connect to all known hubs fail
|
||||
// Adds new hub to local device -> hub cache
|
||||
// Tries to add new hub to local cache of known hubs
|
||||
// Tries to store new hub in Key Vault if it was not yet stored
|
||||
// Sets pooling and correct pool size when building a client
|
||||
// Sets custom retry policy when building a client
|
||||
// Disposes temporary client if an error happens during client build
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TearDownDeviceClientAsync()
|
||||
{
|
||||
// TODO
|
||||
// Multiple calls to TearDownDeviceClientAsync do not run in parallel (test mutual exclusion)
|
||||
// Calls to InitializeDeviceClientAsync and TearDownDeviceClientAsync do not run in parallel (test mutual exclusion)
|
||||
// Removes client from list before closing
|
||||
// Calls CloseAsync before disposing
|
||||
// Disposes client
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ConnectionStatusChange()
|
||||
{
|
||||
// TODO
|
||||
// SDK connection status changes update device status
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"companyName": "Microsoft Corporation",
|
||||
"xmlHeader": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
# Use SDK to build release package
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-img
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
RUN dotnet --info
|
||||
RUN dotnet publish -c Release -o out
|
||||
|
||||
# Use runtime for final image
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
|
||||
WORKDIR /app
|
||||
COPY --from=build-img /app/out ./
|
||||
ENTRYPOINT ["dotnet", "DeviceBridge.dll"]
|
||||
|
||||
EXPOSE 5001
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 96 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 22 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 37 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 86 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 81 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 60 KiB |
|
@ -0,0 +1,683 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "DeviceBridge",
|
||||
"version": "1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/devices/{deviceId}/ConnectionStatus": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ConnectionStatus"
|
||||
],
|
||||
"summary": "Gets that latest connection status for a device.",
|
||||
"description": "For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.",
|
||||
"operationId": "GetCurrentConnectionStatus",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The latest connection status and reason.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceStatusResponseBody"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "If the connection status is not known (i.e., the device hasn't attempted to connect)."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/ConnectionStatus/sub": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ConnectionStatus"
|
||||
],
|
||||
"summary": "Gets the current connection status change subscription for a device.",
|
||||
"operationId": "GetConnectionStatusSubscription",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The current connection status subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscription"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "If a subscription doesn't exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"ConnectionStatus"
|
||||
],
|
||||
"summary": "Creates or updates the current connection status change subscription for a device.",
|
||||
"description": "When the internal connection status of a device changes, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"status\": \"string\",\r\n \"reason\": \"string\"\r\n }\r\n \r\nFor a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.",
|
||||
"operationId": "CreateOrUpdateConnectionStatusSubscription",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created or updated connection status subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscription"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"ConnectionStatus"
|
||||
],
|
||||
"summary": "Deletes the current connection status change subscription for a device.",
|
||||
"operationId": "DeleteConnectionStatusSubscription",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Subscription deleted successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/DeviceBound/sub": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"DeviceBound"
|
||||
],
|
||||
"summary": "Gets the current C2D message subscription for a device.",
|
||||
"operationId": "GetC2DMessageSubscription",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The current C2D message subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "If a subscription doesn't exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"DeviceBound"
|
||||
],
|
||||
"summary": "Creates or updates the current C2D message subscription for a device.",
|
||||
"description": "When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"messageBody\": {},\r\n \"properties\": {\r\n \"prop1\": \"string\",\r\n \"prop2\": \"string\",\r\n },\r\n \"messageId\": \"string\",\r\n \"expirtyTimeUtC\": \"2020-12-04T01:06:14.251Z\"\r\n }\r\n \r\nThe response status code of the callback URL will determine how the service will acknowledge a message:\r\n- Response code between 200 and 299: the service will complete the message.\r\n- Response code between 400 and 499: the service will reject the message.\r\n- Any other response status: the service will abandon the message, causing IotHub to redeliver it.\r\n \r\nFor a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.",
|
||||
"operationId": "CreateOrUpdateC2DMessageSubscription",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created or updated C2D message subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"DeviceBound"
|
||||
],
|
||||
"summary": "Deletes the current C2D message subscription for a device.",
|
||||
"operationId": "DeleteC2DMessageSubscription",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Subscription deleted successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Messages/events": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Messages"
|
||||
],
|
||||
"summary": "Sends a device message to IoTHub.",
|
||||
"description": "Example request:\r\n \r\n POST /devices/{deviceId}/messages/events\r\n {\r\n \"data\": {\r\n \"temperature\": 4.8,\r\n \"humidity\": 31\r\n }\r\n }\r\n.",
|
||||
"operationId": "SendMessage",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/MessageBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Message sent successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Methods/sub": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Methods"
|
||||
],
|
||||
"summary": "Gets the current direct methods subscription for a device.",
|
||||
"operationId": "GetMethodsSubscription",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The current direct methods subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "If a subscription doesn't exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Methods"
|
||||
],
|
||||
"summary": "Creates or updates the current direct methods subscription for a device.",
|
||||
"description": "When the device receives a direct method invocation from IoTHub, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"methodName\": \"string\",\r\n \"requestData\": {}\r\n }\r\n \r\nThe callback may return an optional response body, which will be sent to IoTHub as the method response:\r\n \r\n Example callback response:\r\n {\r\n \"status\": 200,\r\n \"payload\": {}\r\n }\r\n.",
|
||||
"operationId": "CreateOrUpdateMethodsSubscription",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created or updated C2D message subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Methods"
|
||||
],
|
||||
"summary": "Deletes the current direct methods subscription for a device.",
|
||||
"operationId": "DeleteMethodsSubscription",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Subscription deleted successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Registration": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Registration"
|
||||
],
|
||||
"summary": "Performs DPS registration for a device, optionally assigning it to a model.",
|
||||
"description": "The registration result is internally cached to be used in future connections.\r\nThis route is only intended for ahead-of-time registration of devices with the bridge and assignment to a specific model. To access all DPS registration features,\r\nincluding sending custom registration payload and getting the assigned hub, please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).\r\n \r\n<b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a long time to return. If this is a concern, use the DPS REST API directly, which provides\r\nsupport for long-running operation status lookup.",
|
||||
"operationId": "Register",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/RegistrationBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Registration successful."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Resync": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Resync"
|
||||
],
|
||||
"summary": "Forces a full synchronization of all subscriptions for this device and attempts to restart any subscriptions in a stopped state.",
|
||||
"description": "Internally it forces the reconnection of the device if it's in a permanent failure state, due for instance to:\r\n- Bad credentials.\r\n- Device was previously disabled in the cloud side.\r\n- Automatic retries expired (e.g., due to a long period without network connectivity).",
|
||||
"operationId": "Resync",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Resynchronization started."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Twin": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Twin"
|
||||
],
|
||||
"summary": "Gets the device twin.",
|
||||
"operationId": "GetTwin",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The device twin.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"twin": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"desired": {
|
||||
"type": "object"
|
||||
},
|
||||
"reported": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Twin/properties/reported": {
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Twin"
|
||||
],
|
||||
"summary": "Updates reported properties in the device twin.",
|
||||
"description": "Example request:\r\n \r\n PATCH /devices/{deviceId}/properties/reported\r\n {\r\n \"patch\": {\r\n \"fanSpeed\": 35,\r\n \"serial\": \"ABC\"\r\n }\r\n }\r\n.",
|
||||
"operationId": "UpdateReportedProperties",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ReportedPropertiesPatch"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Twin updated successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/devices/{deviceId}/Twin/properties/desired/sub": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Twin"
|
||||
],
|
||||
"summary": "Gets the current desired property change subscription for a device.",
|
||||
"operationId": "GetDesiredPropertiesSubscription",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The current desired property change subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "If a subscription doesn't exist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Twin"
|
||||
],
|
||||
"summary": "Creates or updates the current desired property change subscription for a device.",
|
||||
"description": "When the device receives a new desired property change from IoTHub, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"desiredProperties\": {\r\n \"prop1\": \"string\",\r\n \"prop2\": 12,\r\n \"prop3\": {},\r\n }\r\n }\r\n.",
|
||||
"operationId": "CreateOrUpdateDesiredPropertiesSubscription",
|
||||
"consumes": [
|
||||
"application/json",
|
||||
"text/json",
|
||||
"application/*+json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The created or updated C2D message subscription.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Twin"
|
||||
],
|
||||
"summary": "Deletes the current desired property change subscription for a device.",
|
||||
"operationId": "DeleteDesiredPropertiesSubscription",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "deviceId",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Subscription deleted successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DeviceStatusResponseBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeviceSubscription": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscriptionType": {
|
||||
"type": "string"
|
||||
},
|
||||
"callbackUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SubscriptionCreateOrUpdateBody": {
|
||||
"required": [
|
||||
"callbackUrl"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"callbackUrl": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeviceSubscriptionWithStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscriptionType": {
|
||||
"type": "string"
|
||||
},
|
||||
"callbackUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageBody": {
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": { }
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"componentName": {
|
||||
"type": "string"
|
||||
},
|
||||
"creationTimeUtc": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RegistrationBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"modelId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReportedPropertiesPatch": {
|
||||
"required": [
|
||||
"patch"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"patch": {
|
||||
"type": "object",
|
||||
"additionalProperties": { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
# Azure IoT Central Device Bridge
|
||||
The Device Bridge enables the connection of devices to Azure IoT Central in scenarios where it's not possible to use the Azure IoT Device SDKs.
|
||||
The solution in this repository deploys a set of resources to your Azure Subscription. Once deployed, it exposes a simple, yet powerful, HTTP interface
|
||||
that can be used for sending and receiving data between devices and Azure IoT. The solution can be used *as is* or customized with additional components,
|
||||
such as data transformation and protocol adapter modules.
|
||||
|
||||
- [Deployment instructions](#deployment-instructions)
|
||||
* [1 - Build and push the Docker image](#1---build-and-push-the-docker-image)
|
||||
* [2 - Open the deployment template in Azure Portal](#2---open-the-deployment-template-in-azure-portal)
|
||||
* [3 - Deployment parameters](#3---deployment-parameters)
|
||||
+ [3.1 Bridge name](#3.1-bridge-name)
|
||||
+ [3.2 IoTC SAS key and Id scope](#3.2-iotc-sas-key-and-id-scope)
|
||||
+ [3.3 API key](#3.3-api-key)
|
||||
+ [3.4 SQL credentials](#3.4-sql-credentials)
|
||||
+ [3.5 Log Analytics workspace credentials](#3.5-log-analytics-workspace-credentials)
|
||||
+ [3.6 Image and Azure Container Registry credentials](#3.6-image-and-azure-container-registry-credentials)
|
||||
+ [4 - Create resources](#4---create-resources)
|
||||
- [What is being provisioned](#what-is-being-provisioned)
|
||||
* [Pricing](#pricing)
|
||||
- [HTTP API](#http-api)
|
||||
* [Device to cloud messages](#device-to-cloud-messages)
|
||||
* [Get device twin](#get-device-twin)
|
||||
* [Update reported properties](#update-reported-properties)
|
||||
* [Subscribing to events](#subscribing-to-events)
|
||||
+ [Methods](#methods)
|
||||
+ [C2D messages](#c2d-messages)
|
||||
+ [Desired property updates](#desired-property-updates)
|
||||
* [Device connection status](#device-connection-status)
|
||||
+ [Subscribing to connection status change events](#subscribing-to-connection-status-change-events)
|
||||
+ [Restarting stopped subscriptions and forcing device reconnection](#restarting-stopped-subscriptions-and-forcing-device-reconnection)
|
||||
* [Device provisioning](#device-provisioning)
|
||||
* [Subscription callback retries](#subscription-callback-retries)
|
||||
- [Load and performance](#load-and-performance)
|
||||
* [SSL certificate limits](#ssl-certificate-limits)
|
||||
* [Instance restarts and reconnection speed](#instance-restarts-and-reconnection-speed)
|
||||
* [Multiplexing and connection pool](#multiplexing-and-connection-pool)
|
||||
- [Monitoring](#monitoring)
|
||||
- [Encryption key rotation](#encryption-key-rotation)
|
||||
- [Custom adapters](#custom-adapters)
|
||||
|
||||
## Deployment instructions
|
||||
To use the device bridge solution, you will need the following:
|
||||
- an Azure account. You can create a free Azure account from [here](https://aka.ms/aft-iot)
|
||||
- an Azure IoT Central application to connect the devices. Create a free app by following [these instructions](https://docs.microsoft.com/en-us/azure/iot-central/quick-deploy-iot-central)
|
||||
|
||||
### 1 - Build and push the Docker image
|
||||
First, using the Docker CLI, run the `docker build .` command in the solution folder to build the Device Bridge image. Second, you'll need to
|
||||
tag and push the image to a private container registry, such as Azure Container Registry (ACR). Instructions on how to build, tag, and push an
|
||||
image to ACR can be found [here](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-docker-cli).
|
||||
Note the image name and the ACR credentials, which are necessary in the next steps.
|
||||
|
||||
### 2 - Open the deployment template in Azure Portal
|
||||
Open the [template deployment page in the Azure Portal](https://portal.azure.com/#create/Microsoft.Template), click `Build your own template in the editor`.
|
||||
In the edit template page, paste the contents of the `azuredeploy.json` file in this repository in the editor window and click save.
|
||||
|
||||
![Custom deployment](Docs/Assets/custom_deployment.png "custom deployment")
|
||||
![Edit template](Docs/Assets/edit_template.png "Edit template")
|
||||
|
||||
### 3 - Deployment parameters
|
||||
|
||||
#### 3.1 Bridge name
|
||||
In the `Bridge-name` parameter, enter the name for the new Bridge instance. This name will be part of the HTTPS endpoint
|
||||
for your instance, so it may only contain letters, numbers, and dashes.
|
||||
|
||||
#### 3.2 IoTC SAS key and Id scope
|
||||
These parameters link your new Bridge instance to a specific Azure IoT Central application. Go to your IoT Central
|
||||
application and navigate to the Administration > Device Connection area. Copy the `ID Scope` field and paste it into
|
||||
the `iotc-id-scope` parameter in the template.
|
||||
In the same page, under Enrollment groups, open the `SAS-IoT-Devices` group. In the group page, copy either the `Primary key` or
|
||||
`Secondary key` and paste it in the `Iotc-dps-sas-key` parameter of the template (this key will be stored in a Key Vault provisioned with the solution).
|
||||
|
||||
![Scope Id and key](Docs/Assets/scope_id_and_key.png "scope id and key")
|
||||
![Enrollment group](Docs/Assets/sas_enrollment_group.png "enrollment group")
|
||||
|
||||
#### 3.3 API key
|
||||
In the `Api-key` parameter, define a strong (and preferably randomly generated) API key. This key will be used to protect the HTTP API and must be included in the
|
||||
`x-api-key` header of every request to the Bridge.
|
||||
|
||||
#### 3.4 SQL credentials
|
||||
In the `Sql-username` and `Sql-password` parameters, enter an admin login and password for the Azure SQL instance that will be provisioned
|
||||
by the solution. The password must follow the [SQL Server password policy](https://docs.microsoft.com/en-us/sql/relational-databases/security/password-policy?view=sql-server-ver15#password-complexity).
|
||||
|
||||
#### 3.5 Log Analytics workspace credentials
|
||||
By default, the Bridge will publish logs to a Log Analytics workspace of your choice. In the `Log-analytics-workspace-id` and `Log-analytics-workspace-key`
|
||||
parameters, provide the credentials of your workspace (instructions to obtain the credentials of your Log Analytics workspace can be found [here](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-log-analytics#get-log-analytics-credentials)).
|
||||
|
||||
#### 3.6 Image and Azure Container Registry credentials
|
||||
In this step you need to grant the solution access to the Docker image that you built and pushed in step 1.
|
||||
In the `Bridge-image` enter the name of the image that you pushed to your private ACR. In the `Acr-server`,
|
||||
`Acr-username`, and `Acr-password` parameters provide the credentials of your ACR instance.
|
||||
|
||||
#### 4 - Create resources
|
||||
After providing all necessary parameters, click the `Review+create` button. The deployment may take a few minutes. Once finished,
|
||||
you can find the newly provisioned resources in the target resource group.
|
||||
|
||||
## What is being provisioned
|
||||
The template in this solution will provision the following resources to your Azure subscription:
|
||||
- Key Vault - to store all secrets
|
||||
- Azure SQL - to store device and subscription data
|
||||
- Azure container instance - main solution
|
||||
- Azure container instance - setup - (only runs during the solution setup phase)
|
||||
|
||||
### Pricing
|
||||
The main pricing components for the resources published by the ARM template in this repository are the Azure Container Instance that
|
||||
hosts the core Bridge (1 CPU and 3 GB of memory - see [Container Instances pricing](https://azure.microsoft.com/en-us/pricing/details/container-instances/)
|
||||
for an estimate) and the Azure SQL Database (Basic tier, 5 DTUs - see [Azure SQL pricing](https://azure.microsoft.com/en-us/pricing/details/sql-database/single/) for more details).
|
||||
|
||||
## HTTP API
|
||||
Once deployed, the solution will expose an HTTPS endpoint at `https://<bridge-name>.<region>.azurecontainer.io`. This endpoint can also be found by navigating to
|
||||
the Device Bridge container group deployed to your subscription > `Properties` > `FQDN`.
|
||||
|
||||
The HTTP endpoints are protected by the API key provided when the solution was provisioned. For every request, include an `x-api-key` header containing the key.
|
||||
|
||||
In what follows we give a quick overview of the functionalities available through the API. For a full description of the
|
||||
endpoints and data types as well as sample requests and responses, refer to the API swagger under `Docs/swagger.json`.
|
||||
|
||||
### Device to cloud messages
|
||||
The Bridge can send messages to Azure IoT on behalf of a device.
|
||||
The main component of a message is the `data` field. Optionally, the request can include `componentName`, `properties`, and a `creationTimeUtc`.
|
||||
Here's an example of a request to send a message:
|
||||
|
||||
```json
|
||||
POST /devices/{deviceId}/messages/events
|
||||
{
|
||||
"data": {
|
||||
"temperature": 4.8,
|
||||
"humidity": 31
|
||||
},
|
||||
"properties": {
|
||||
"prop1": "abc"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Get device twin
|
||||
The device twin will include the latest version of desired and reported properties of a device.
|
||||
Below is an example of the response returned by the service when fetching the latest twin for a device:
|
||||
|
||||
```json
|
||||
{
|
||||
"twin": {
|
||||
"properties": {
|
||||
"desired": {
|
||||
"fanSpeed": 30,
|
||||
"$version": 2
|
||||
},
|
||||
"reported": {
|
||||
"threshold": 4.8,
|
||||
"LastReported": "2021-01-10T00:35:00.388Z",
|
||||
"$version": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update reported properties
|
||||
To update reported properties in a device twin, include a patch in the request body, as the example below:
|
||||
|
||||
```json
|
||||
PATCH /devices/{deviceId}/properties/reported
|
||||
{
|
||||
"patch": {
|
||||
"fanSpeed": 35,
|
||||
"serial": "ABC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Subscribing to events
|
||||
You can subscribe to get notified of cloud-to-device events (methods, C2D messages, and desired property updates). To create a subscription,
|
||||
you need to provide the callback URL that the service will send a `POST` request to when a notification is available. Only one subscription of each type can exist at a time
|
||||
(e.g., issuing a new subscription creation request of the same type will update the callback URL of the existing subscription).
|
||||
|
||||
#### Methods
|
||||
Method subscriptions will emit an event whenever a new direct method invocation is issues by the cloud. The following is
|
||||
an example of a method invocation event:
|
||||
|
||||
```json
|
||||
{
|
||||
"eventType": "DirectMethodInvocation",
|
||||
"deviceId": "my-device",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"methodName": "increaseTemperature",
|
||||
"requestData": {
|
||||
"celsius" : 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The callback may return an optional response body, which will be sent to IoTHub as the method response.
|
||||
Below is an example of a response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"payload": {
|
||||
"newTemperature": 24
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### C2D messages
|
||||
When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL as in the example below:
|
||||
|
||||
```json
|
||||
{
|
||||
"eventType": "C2DMessage",
|
||||
"deviceId": "my-device",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"messageBody": {
|
||||
"someField": 20.2
|
||||
},
|
||||
"properties": {
|
||||
"prop1": "val1",
|
||||
"prop2": "val2",
|
||||
},
|
||||
"messageId": "abc",
|
||||
"expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
|
||||
}
|
||||
```
|
||||
|
||||
The response status code of the callback URL will determine how the service will acknowledge a message:
|
||||
- Response code between 200 and 299: the service will `complete` the message.
|
||||
- Response code between 400 and 499: the service will `reject` the message.
|
||||
- Any other response status: the service will `abandon` the message, causing IotHub to redeliver it.
|
||||
|
||||
For a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
|
||||
|
||||
#### Desired property updates
|
||||
When the device receives a new desired property change from IoT Hub, the service will send an event to the desired callback URL. Example event:
|
||||
|
||||
```json
|
||||
{
|
||||
"eventType": "DesiredPropertyUpdate",
|
||||
"deviceId": "my-device",
|
||||
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
"desiredProperties": {
|
||||
"prop1": "string",
|
||||
"prop2": 12,
|
||||
"prop3": {},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> NOTE: desired property updates are only received while the device is connected to IoT Hub. For this reason, property updates
|
||||
might not be received while, for instance, the device is internally reconnecting due to a transient network error. To mitigate scenarios like this,
|
||||
you can listen to internal connection events, as described in the next section, and get the latest device twin whenever the `Connected` event
|
||||
is received.
|
||||
|
||||
### Device connection status
|
||||
Internally, the Device Bridge connects devices to Azure IoT through AMQP using connection multiplexing. It transparently manages
|
||||
the life cycle of all connections (i.e., connecting, disconnecting, and retrying on transient errors). When you issue a command,
|
||||
such as sending a message or getting the device twin, the Bridge opens
|
||||
a temporary connection for the device that is set to live between 9 to 11 minutes and is renewed as requests come. For subscriptions,
|
||||
the Bridge will open a permanent connection for the device. This connection will last until the last subscription is deleted.
|
||||
|
||||
Depending on the connection status of a device, it's subscriptions may have one of the following status:
|
||||
- `Starting`: initial status after a subscription is created and before the device has been connected internally.
|
||||
This status may also represent that the Bridge is reconnecting the device after the service restarts.
|
||||
- `Running`: the device is connected and events are flowing.
|
||||
- `Stopped`: the device is disconnected due to a permanent failure and events are not flowing. Permanent failure scenarios can include: the device has been disabled in the IoT Hub, credentials are no longer valid, the automatic connection retries
|
||||
expired due to a long period without network connectivity, etc.
|
||||
|
||||
#### Subscribing to connection status change events
|
||||
In many situations it may be useful to react to changes in the underlying device connection status. For instance,
|
||||
you may want to fetch the latest device twin whenever the device reconnects internally. Alternatively, you may want to
|
||||
be notified when a device experiences a permanent failure, such as credentials expired. For these scenarios you can subscribe
|
||||
to connection status changes.
|
||||
|
||||
Whenever the device internally connects or reconnects, a `Connected` event will be sent. If the device fails to connect permanently,
|
||||
a `Disconnected` event will be emitted, including an associated failure reason. Other possible statuses are `Disabled` (the connection has
|
||||
been closed properly) and `Disconnected_Retrying` (the service is retrying to connect the device).
|
||||
For a detailed description of all status and reasons, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
|
||||
#### Restarting stopped subscriptions and forcing device reconnection
|
||||
Issuing a call to the `resync` endpoint will cause the Bridge to attempt to reconnect any device previously in a permanent failure state.
|
||||
If the device reconnection is successful, all stopped subscriptions will be back to a running state. You may want to issue this command, for instance,
|
||||
after re-enabling a device in IoT Hub.
|
||||
|
||||
### Device provisioning
|
||||
By default, issuing a command to the Bridge on behalf of a device will cause it to be automatically provisioned. The device
|
||||
is provisioned using the SAS key provided during the solution deployment and will not be assigned to any particular model.
|
||||
The Bridge provides an endpoint to optionally provision a device to a specific model ahead of time. The registration result is
|
||||
internally cached to be used in future connections.
|
||||
|
||||
To access advanced registration features, including sending custom registration payload and getting the assigned hub,
|
||||
please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
|
||||
|
||||
> NOTE: DPS registration is a long-running operation, so calls to this route may take a long time to return (in the order of seconds).
|
||||
If this is a concern, use the DPS REST API directly, which provides support for long-running operation status lookup.
|
||||
|
||||
### Subscription callback retries
|
||||
The solution will automatically retry if a transient error happens when calling a subscription callback endpoint. By default, it will
|
||||
retry 5 times with an exponentially increasing interval (total 30 seconds). This number can be customized by overriding the `HTTP_RETRY_LIMIT`
|
||||
environment variable in the provisioning template. For `HTTP 429s`, the service will respect the `Retry-After` header, if one is available.
|
||||
|
||||
> NOTE: if needed, the solution code can be modified to add HTTP circuit-breaking be following the instructions on https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-circuit-breaker-pattern.
|
||||
|
||||
## Load and performance
|
||||
The default setup provisioned by the ARM template in this repository was tested with up to 50,000 devices simultaneously connected.
|
||||
In this scenario, we were able to send 10,000 device to cloud messages (telemetry) per minute through the Bridge. For cloud to device
|
||||
events (including C2D messages, method invocations, and desired property updates combined), the Bridge was able to receive and forward 5,000
|
||||
events per minute.
|
||||
|
||||
This test was performed in a constant-rate scenario, where all devices were already internally connected. Numbers may greatly vary depending
|
||||
on different usage patterns, such as how often devices send or subscribe to events, which affects how often they are internally reconnected.
|
||||
The specific setup being provisioned (e.g., different container sizes) will also directly influence the results. When deploying a new setup,
|
||||
make sure you evaluate the performance considerations described in the following sections.
|
||||
|
||||
### SSL certificate limits
|
||||
The Caddy webserver deployed in the solution uses [Let's Encrypt](https://letsencrypt.org/) to automatically obtain an HTTPS certificate.
|
||||
This service is free, but has a current limit of SSL 5 certificates per week per endpoint. Because the containers used by this solution
|
||||
deploy non-persistent volumes, if the container group is manually restarted more than 5 times in a week (for instance during development phase),
|
||||
SSL certificate acquisition will fail and a new solution with a new endpoint will need to be deployed.
|
||||
|
||||
To avoid this limitation, you may choose to modify the solution to [use persistent storage](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-volume-azure-files),
|
||||
use a different [certificate authority and webserver configuration](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-container-group-ssl),
|
||||
or use Let's Encrypt staging environment during development (see https://letsencrypt.org/docs/rate-limits/ for more information).
|
||||
|
||||
### Instance restarts and reconnection speed
|
||||
If the service instance restarts, the Bridge will automatically reconnect any devices with an active subscription. During this period,
|
||||
all subscriptions will have a `Starting` status. The speed at which the Bridge reconnects devices is set by default to 150 connections per second.
|
||||
This means that, in the event of a container restart, it would take around one minute to reconnect 10,000 devices.
|
||||
|
||||
> NOTE: this number can be customized through `DEVICE_RAMPUP_BATCH_SIZE` and `DEVICE_RAMPUP_BATCH_INTERVAL_MS` environment variables. However, before adjusting this
|
||||
values it's important to perform a load test to make sure that the service can support the desired reconnection speed. Factors that may influence this speed is
|
||||
the size of the hosting container, number of devices, and desired number of connections in the multiplexing pool.
|
||||
|
||||
### Multiplexing and connection pool
|
||||
By default, the Bridge is configured to use a pool of 50 active AMQP connections. This amount allows around 50K devices to be connected
|
||||
simultaneously. This number can be configured through the `MAX_POOL_SIZE` environment variable, however increasing this value may require
|
||||
decreasing the default device reconnection speed, or else the Bridge may fail to reconnect devices in the event of a container restart.
|
||||
For more details about connection pooling and multiplexing, see https://github.com/Azure/azure-iot-sdk-csharp/blob/master/iothub/device/devdoc/amqpstack.md.
|
||||
|
||||
> NOTE: this solution is meant to run as a single instance. Deploying more than one instance of the service pointing to the same
|
||||
Azure IoT Central application will potentially cause duplicate device connections, as instances are not aware of each other. This will
|
||||
result in connection drops, as each device can only have a single active AMQP link to IoT Hub at all times.
|
||||
|
||||
## Monitoring
|
||||
Service logs are emitted to the Log Analytics Workspace provided during deployment. You can use the logs to query device events as well as
|
||||
build monitors for specific scenarios. Below is an example query that shows all device events in the past hour:
|
||||
|
||||
```
|
||||
ContainerInstanceLog_CL
|
||||
| where TimeGenerated >= ago(1h)
|
||||
| where ContainerGroup_s == "<container-group-name>"
|
||||
| extend log = parse_json(Message)
|
||||
| extend deviceId = tostring(log.deviceId)
|
||||
| extend _message = tostring(log.message)
|
||||
| where deviceId != ""
|
||||
| distinct TimeGenerated, deviceId, _message
|
||||
```
|
||||
|
||||
## Encryption key rotation
|
||||
When a new subscription is created, the provided callback URL is stored in the database in an encrypted format. This provides an
|
||||
extra layer of security, as these URLs may contain sensitive data, such as access keys or tokens in the query parameters. The
|
||||
encryption key used in this scenario is automatically generated when the solution is deployed and stored in the Key Vault.
|
||||
|
||||
To rotate this encryption key, simply start the setup container provisioned to the same resource group (the container group name
|
||||
starts with `iotc-container-groups-setup-`). Once started, the setup container will automatically generate a new encryption key and
|
||||
use it to reencrypt all subscriptions stored in the database (you can follow the progress in the container logs `Container group > Containers > Logs`).
|
||||
Once reencryption has finished, you can restart the main Bridge container (the container group name starts with `iotc-container-groups-`)
|
||||
to make sure that it starts using the new key for new subscriptions.
|
||||
|
||||
## Custom adapters
|
||||
One of the possible ways to extend the functionality of this Bridge is through a custom adapter deployed as a sidecar container.
|
||||
With this type of adapter, which can be written in the language and runtime of your choice, you can for instance transform
|
||||
the data before it reaches or leaves the Bridge and connect to different data sources. Under the `Samples/SampleTypeScriptAdapter`
|
||||
folder we provide an example of a custom adapter written in TypeScript that forwards cloud-to-device messages to an EventHub.
|
||||
This adapter uses a client automatically generated with [AutoRest](https://github.com/Azure/autorest) using the Bridge swagger available
|
||||
under `Docs/swagger.json`. The example also contains the necessary ARM template to deploy it as a sidecar with the Bridge.
|
||||
|
||||
The diagrams below illustrate the original Bridge architecture and how a custom adapter fits into it:
|
||||
|
||||
![Original architecture](Docs/Assets/original-architecture.png "original architecture")
|
||||
![Adapter architecture](Docs/Assets/adapter-architecture.png "adapter architecture")
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
dist/
|
||||
/package-lock.json
|
|
@ -0,0 +1,9 @@
|
|||
FROM node:12
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm i --quiet && npm cache clean --force
|
||||
COPY ./ ./
|
||||
RUN npm run build
|
|
@ -0,0 +1,23 @@
|
|||
# Sample adapter for the Device Bridge
|
||||
This sample adapter written in TypeScript forwards D2C messages to an Event Hub.
|
||||
The code uses a client automatically generated by autorest using the Device Bridge swagger.
|
||||
Failures are logged in the same Log Analytics workspace used by the Bridge core module.
|
||||
|
||||
> NOTE: this code should only be used as a sample reference for how to build adapters for the Device
|
||||
Bridge. It should not be used in a production setting without proper testing and error handling.
|
||||
|
||||
## APIs
|
||||
|
||||
### Subscribe
|
||||
```
|
||||
POST /subscribe/{deviceId}
|
||||
```
|
||||
Creates a C2D message subscription for a device and forwards all C2D messages to an EventHub.
|
||||
|
||||
|
||||
### Unsubscribe
|
||||
```
|
||||
POST /unsubscribe/{deviceId}
|
||||
```
|
||||
|
||||
Deletes the C2D message subscription for a device and stops publishing events.
|
|
@ -0,0 +1,535 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"bridge-name": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "The name of the device bridge. Also will be used in the url."
|
||||
}
|
||||
},
|
||||
"iotc-dps-sas-key": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "DPS sas key for provisioning devices and sending data. Retrieved from iot central."
|
||||
}
|
||||
},
|
||||
"iotc-id-scope": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "ID Scope provisioning devices and sending data. Retrieved from iot central."
|
||||
}
|
||||
},
|
||||
"event-hub-name": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Name of the Event Hub to push events to."
|
||||
}
|
||||
},
|
||||
"event-hub-connection-string": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Connection string of the Event Hub to push events to."
|
||||
}
|
||||
},
|
||||
"api-key": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Api key used to validate requests to IoTC device bridge."
|
||||
}
|
||||
},
|
||||
"sql-username": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Username for the sql server provisioned."
|
||||
}
|
||||
},
|
||||
"sql-password": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Password for the sql server provisioned."
|
||||
}
|
||||
},
|
||||
"log-analytics-workspace-id": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Log Analytics workspace id for log storage."
|
||||
}
|
||||
},
|
||||
"log-analytics-workspace-key": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Log Analytics workspace key for log storage."
|
||||
}
|
||||
},
|
||||
"bridge-image": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Docker image to be deployed for the core and setup modules."
|
||||
}
|
||||
},
|
||||
"adapter-image": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Docker image to be deployed for the adapter module."
|
||||
}
|
||||
},
|
||||
"acr-server": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Private ACR server to pull the image from."
|
||||
}
|
||||
},
|
||||
"acr-username": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "Username of the private ACR."
|
||||
}
|
||||
},
|
||||
"acr-password": {
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Password of the private ACR."
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"setupContainerGroupsName": "[concat('iotc-container-groups-setup-', uniqueString(resourceGroup().id))]",
|
||||
"containerGroupsName": "[concat('iotc-container-groups-', uniqueString(resourceGroup().id))]",
|
||||
"bridgeContainerName": "[concat('iotc-bridge-container-', uniqueString(resourceGroup().id))]",
|
||||
"keyvaultName": "[concat('iotc-kv-', uniqueString(resourceGroup().id))]",
|
||||
"databaseName": "[concat('iotc-db-', uniqueString(resourceGroup().id))]",
|
||||
"sqlServerName": "[concat('iotc-sql-', uniqueString(resourceGroup().id))]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Sql/servers",
|
||||
"apiVersion": "2019-06-01-preview",
|
||||
"name": "[variables('sqlServerName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"tags": {},
|
||||
"kind": "v12.0",
|
||||
"identity": {
|
||||
"type": "SystemAssigned"
|
||||
},
|
||||
"properties": {
|
||||
"administratorLogin": "[parameters('sql-username')]",
|
||||
"administratorLoginPassword": "[parameters('sql-password')]",
|
||||
"version": "12.0",
|
||||
"publicNetworkAccess": "Enabled"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "firewallrules",
|
||||
"name": "AllowAllWindowsAzureIps",
|
||||
"apiVersion": "2019-06-01-preview",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Sql/servers', concat(variables('sqlServerName')))]"
|
||||
],
|
||||
"properties": {
|
||||
"endIpAddress": "0.0.0.0",
|
||||
"startIpAddress": "0.0.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "databases",
|
||||
"apiVersion": "2019-06-01-preview",
|
||||
"name": "[variables('databaseName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]"
|
||||
],
|
||||
"sku": {
|
||||
"name": "Basic",
|
||||
"tier": "Basic",
|
||||
"capacity": 5
|
||||
},
|
||||
"kind": "v12.0,user",
|
||||
"properties": {
|
||||
"collation": "SQL_Latin1_General_CP1_CI_AS",
|
||||
"maxSizeBytes": 2147483648,
|
||||
"catalogCollation": "SQL_Latin1_General_CP1_CI_AS",
|
||||
"zoneRedundant": false,
|
||||
"readScale": "Disabled",
|
||||
"storageAccountType": "GRS"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ContainerInstance/containerGroups",
|
||||
"apiVersion": "2019-12-01",
|
||||
"name": "[variables('setupContainerGroupsName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Sql/servers/databases', variables('sqlServerName'), variables('databaseName'))]"
|
||||
],
|
||||
"identity": {
|
||||
"type": "SystemAssigned"
|
||||
},
|
||||
"properties": {
|
||||
"sku": "Standard",
|
||||
"containers": [
|
||||
{
|
||||
"name": "bridge-setup",
|
||||
"properties": {
|
||||
"image": "[parameters('bridge-image')]",
|
||||
"environmentVariables": [
|
||||
{
|
||||
"name": "KV_URL",
|
||||
"value": "[concat('https://', variables('keyvaultName'), '.vault.azure.net/')]"
|
||||
}
|
||||
],
|
||||
"command": [
|
||||
"dotnet",
|
||||
"DeviceBridge.dll",
|
||||
"--setup"
|
||||
],
|
||||
"resources": {
|
||||
"requests": {
|
||||
"memoryInGB": 1,
|
||||
"cpu": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"restartPolicy": "OnFailure",
|
||||
"imageRegistryCredentials": [
|
||||
{
|
||||
"server": "[parameters('acr-server')]",
|
||||
"username": "[parameters('acr-username')]",
|
||||
"password": "[parameters('acr-password')]"
|
||||
}
|
||||
],
|
||||
"diagnostics" : {
|
||||
"logAnalytics": {
|
||||
"workspaceId": "[parameters('log-analytics-workspace-id')]",
|
||||
"workspaceKey": "[parameters('log-analytics-workspace-key')]"
|
||||
}
|
||||
},
|
||||
"osType": "Linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.ContainerInstance/containerGroups",
|
||||
"apiVersion": "2019-12-01",
|
||||
"name": "[variables('containerGroupsName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Sql/servers/databases', variables('sqlServerName'), variables('databaseName'))]"
|
||||
],
|
||||
"identity": {
|
||||
"type": "SystemAssigned"
|
||||
},
|
||||
"properties": {
|
||||
"sku": "Standard",
|
||||
"containers": [
|
||||
{
|
||||
"name": "adapter",
|
||||
"properties": {
|
||||
"image": "[parameters('adapter-image')]",
|
||||
"ports": [
|
||||
{
|
||||
"port": 3000
|
||||
},
|
||||
{
|
||||
"port": 3001
|
||||
}
|
||||
],
|
||||
"environmentVariables": [
|
||||
{
|
||||
"name": "EVENTHUB_CONNECTION_STRING",
|
||||
"value": "[parameters('event-hub-connection-string')]"
|
||||
},
|
||||
{
|
||||
"name": "EVENTHUB_NAME",
|
||||
"value": "[parameters('event-hub-name')]"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"requests": {
|
||||
"memoryInGB": 0.5,
|
||||
"cpu": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "[variables('bridgeContainerName')]",
|
||||
"properties": {
|
||||
"image": "[parameters('bridge-image')]",
|
||||
"ports": [
|
||||
{
|
||||
"port": 5001
|
||||
}
|
||||
],
|
||||
"environmentVariables": [
|
||||
{
|
||||
"name": "MAX_POOL_SIZE",
|
||||
"value": "50"
|
||||
},
|
||||
{
|
||||
"name": "DEVICE_RAMPUP_BATCH_SIZE",
|
||||
"value": "150"
|
||||
},
|
||||
{
|
||||
"name": "DEVICE_RAMPUP_BATCH_INTERVAL_MS",
|
||||
"value": "1000"
|
||||
},
|
||||
{
|
||||
"name": "KV_URL",
|
||||
"value": "[concat('https://', variables('keyvaultName'), '.vault.azure.net/')]"
|
||||
},
|
||||
{
|
||||
"name": "PORT",
|
||||
"value": "5001"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"requests": {
|
||||
"memoryInGB": 1.5,
|
||||
"cpu": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "caddy-ssl-server",
|
||||
"properties": {
|
||||
"image": "caddy:latest",
|
||||
"command": [
|
||||
"caddy",
|
||||
"reverse-proxy",
|
||||
"--from",
|
||||
"[concat(parameters('bridge-name'), '.', resourceGroup().location, '.azurecontainer.io')]",
|
||||
"--to",
|
||||
"localhost:3000"
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"protocol": "TCP",
|
||||
"port": 443
|
||||
},
|
||||
{
|
||||
"protocol": "TCP",
|
||||
"port": 80
|
||||
}
|
||||
],
|
||||
"environmentVariables": [],
|
||||
"resources": {
|
||||
"requests": {
|
||||
"memoryInGB": 0.5,
|
||||
"cpu": 0.2
|
||||
}
|
||||
},
|
||||
"volumeMounts": [
|
||||
{
|
||||
"name": "data",
|
||||
"mountPath": "/data"
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"mountPath": "/config"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"name": "data",
|
||||
"emptyDir": {}
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"emptyDir": {}
|
||||
}
|
||||
],
|
||||
"initContainers": [
|
||||
],
|
||||
"restartPolicy": "Always",
|
||||
"imageRegistryCredentials": [
|
||||
{
|
||||
"server": "[parameters('acr-server')]",
|
||||
"username": "[parameters('acr-username')]",
|
||||
"password": "[parameters('acr-password')]"
|
||||
}
|
||||
],
|
||||
"diagnostics" : {
|
||||
"logAnalytics": {
|
||||
"workspaceId": "[parameters('log-analytics-workspace-id')]",
|
||||
"workspaceKey": "[parameters('log-analytics-workspace-key')]"
|
||||
}
|
||||
},
|
||||
"ipAddress": {
|
||||
"ports": [
|
||||
{
|
||||
"protocol": "TCP",
|
||||
"port": 443
|
||||
}
|
||||
],
|
||||
"type": "Public",
|
||||
"dnsNameLabel": "[parameters('bridge-name')]"
|
||||
},
|
||||
"osType": "Linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.KeyVault/vaults",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "[variables('keyvaultName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.ContainerInstance/containerGroups', variables('containerGroupsName'))]",
|
||||
"[resourceId('Microsoft.ContainerInstance/containerGroups', variables('setupContainerGroupsName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"sku": {
|
||||
"family": "A",
|
||||
"name": "Standard"
|
||||
},
|
||||
"tenantId": "[subscription().tenantId]",
|
||||
"enabledForDeployment": true,
|
||||
"enabledForDiskEncryption": true,
|
||||
"enabledForTemplateDeployment": true,
|
||||
"accessPolicies": [
|
||||
{
|
||||
"tenantId": "[subscription().tenantid]",
|
||||
"objectId": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', variables('containerGroupsName')),'2019-12-01', 'full').identity.principalId]",
|
||||
"permissions": {
|
||||
"keys": [],
|
||||
"secrets": [
|
||||
"get",
|
||||
"list"
|
||||
],
|
||||
"certificates": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tenantId": "[subscription().tenantid]",
|
||||
"objectId": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', variables('setupContainerGroupsName')),'2019-12-01', 'full').identity.principalId]",
|
||||
"permissions": {
|
||||
"keys": [],
|
||||
"secrets": [
|
||||
"get",
|
||||
"list",
|
||||
"set"
|
||||
],
|
||||
"certificates": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "apiKey",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[parameters('api-key')]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "iotc-sas-key",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[parameters('iotc-dps-sas-key')]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "iotc-id-scope",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[parameters('iotc-id-scope')]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "sql-username",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[parameters('sql-username')]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "sql-password",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[parameters('sql-password')]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "sql-server",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]",
|
||||
"[resourceId('Microsoft.Sql/servers', concat(variables('sqlServerName')))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[reference(variables('sqlServerName')).fullyQualifiedDomainName]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "secrets",
|
||||
"apiVersion": "2016-10-01",
|
||||
"name": "sql-database",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"value": "[variables('databaseName')]",
|
||||
"attributes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "sample-adapter",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@azure/event-hubs": "^5.3.1",
|
||||
"@azure/ms-rest-js": "^2.1.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/node": "^14.14.10"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,957 @@
|
|||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
*/
|
||||
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
import * as Models from "./models";
|
||||
import * as Mappers from "./models/mappers";
|
||||
import * as Parameters from "./models/parameters";
|
||||
import { DeviceBridgeContext } from "./deviceBridgeContext";
|
||||
|
||||
class DeviceBridge extends DeviceBridgeContext {
|
||||
/**
|
||||
* Initializes a new instance of the DeviceBridge class.
|
||||
* @param [options] The parameter options
|
||||
*/
|
||||
constructor(options?: Models.DeviceBridgeOptions) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a detailed description of each status, see
|
||||
* https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
* @summary Gets that latest connection status for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.GetCurrentConnectionStatusResponse>
|
||||
*/
|
||||
getCurrentConnectionStatus(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetCurrentConnectionStatusResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
getCurrentConnectionStatus(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceStatusResponseBody>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
getCurrentConnectionStatus(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceStatusResponseBody>): void;
|
||||
getCurrentConnectionStatus(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceStatusResponseBody>, callback?: msRest.ServiceCallback<Models.DeviceStatusResponseBody>): Promise<Models.GetCurrentConnectionStatusResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
getCurrentConnectionStatusOperationSpec,
|
||||
callback) as Promise<Models.GetCurrentConnectionStatusResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets the current connection status change subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.GetConnectionStatusSubscriptionResponse>
|
||||
*/
|
||||
getConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetConnectionStatusSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
getConnectionStatusSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
getConnectionStatusSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
|
||||
getConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscription>, callback?: msRest.ServiceCallback<Models.DeviceSubscription>): Promise<Models.GetConnectionStatusSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
getConnectionStatusSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.GetConnectionStatusSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the internal connection status of a device changes, the service will send an event to the
|
||||
* desired callback URL.
|
||||
*
|
||||
* Example event:
|
||||
* {
|
||||
* "eventType": "string",
|
||||
* "deviceId": "string",
|
||||
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
* "status": "string",
|
||||
* "reason": "string"
|
||||
* }
|
||||
*
|
||||
* For a detailed description of each status, see
|
||||
* https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
|
||||
* @summary Creates or updates the current connection status change subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse>
|
||||
*/
|
||||
createOrUpdateConnectionStatusSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams): Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateConnectionStatusSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateConnectionStatusSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
|
||||
createOrUpdateConnectionStatusSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscription>, callback?: msRest.ServiceCallback<Models.DeviceSubscription>): Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
createOrUpdateConnectionStatusSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Deletes the current connection status change subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
deleteConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteConnectionStatusSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteConnectionStatusSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
|
||||
deleteConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
deleteConnectionStatusSubscriptionOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets the current C2D message subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.GetC2DMessageSubscriptionResponse>
|
||||
*/
|
||||
getC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetC2DMessageSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
getC2DMessageSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
getC2DMessageSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
getC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.GetC2DMessageSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
getC2DMessageSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.GetC2DMessageSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the device receives a new C2D message from IoTHub, the service will send an event to the
|
||||
* desired callback URL.
|
||||
*
|
||||
* Example event:
|
||||
* {
|
||||
* "eventType": "string",
|
||||
* "deviceId": "string",
|
||||
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
* "messageBody": {},
|
||||
* "properties": {
|
||||
* "prop1": "string",
|
||||
* "prop2": "string",
|
||||
* },
|
||||
* "messageId": "string",
|
||||
* "expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
|
||||
* }
|
||||
*
|
||||
* The response status code of the callback URL will determine how the service will acknowledge a
|
||||
* message:
|
||||
* - Response code between 200 and 299: the service will complete the message.
|
||||
* - Response code between 400 and 499: the service will reject the message.
|
||||
* - Any other response status: the service will abandon the message, causing IotHub to redeliver
|
||||
* it.
|
||||
*
|
||||
* For a detailed overview of C2D messages, see
|
||||
* https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
|
||||
* @summary Creates or updates the current C2D message subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse>
|
||||
*/
|
||||
createOrUpdateC2DMessageSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams): Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateC2DMessageSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateC2DMessageSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
createOrUpdateC2DMessageSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
createOrUpdateC2DMessageSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Deletes the current C2D message subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
deleteC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteC2DMessageSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteC2DMessageSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
|
||||
deleteC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
deleteC2DMessageSubscriptionOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example request:
|
||||
*
|
||||
* POST /devices/{deviceId}/messages/events
|
||||
* {
|
||||
* "data": {
|
||||
* "temperature": 4.8,
|
||||
* "humidity": 31
|
||||
* }
|
||||
* }
|
||||
* .
|
||||
* @summary Sends a device message to IoTHub.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
sendMessage(deviceId: string, options?: Models.DeviceBridgeSendMessageOptionalParams): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
sendMessage(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
sendMessage(deviceId: string, options: Models.DeviceBridgeSendMessageOptionalParams, callback: msRest.ServiceCallback<void>): void;
|
||||
sendMessage(deviceId: string, options?: Models.DeviceBridgeSendMessageOptionalParams | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
sendMessageOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets the current direct methods subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.GetMethodsSubscriptionResponse>
|
||||
*/
|
||||
getMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetMethodsSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
getMethodsSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
getMethodsSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
getMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.GetMethodsSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
getMethodsSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.GetMethodsSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the device receives a direct method invocation from IoTHub, the service will send an event
|
||||
* to the desired callback URL.
|
||||
*
|
||||
* Example event:
|
||||
* {
|
||||
* "eventType": "string",
|
||||
* "deviceId": "string",
|
||||
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
* "methodName": "string",
|
||||
* "requestData": {}
|
||||
* }
|
||||
*
|
||||
* The callback may return an optional response body, which will be sent to IoTHub as the method
|
||||
* response:
|
||||
*
|
||||
* Example callback response:
|
||||
* {
|
||||
* "status": "string",
|
||||
* "payload": {}
|
||||
* }
|
||||
* .
|
||||
* @summary Creates or updates the current direct methods subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.CreateOrUpdateMethodsSubscriptionResponse>
|
||||
*/
|
||||
createOrUpdateMethodsSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams): Promise<Models.CreateOrUpdateMethodsSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateMethodsSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateMethodsSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
createOrUpdateMethodsSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.CreateOrUpdateMethodsSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
createOrUpdateMethodsSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.CreateOrUpdateMethodsSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Deletes the current direct methods subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
deleteMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteMethodsSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteMethodsSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
|
||||
deleteMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
deleteMethodsSubscriptionOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* The registration result is internally cached to be used in future connections.
|
||||
* This route is only intended for ahead-of-time registration of devices with the bridge and
|
||||
* assignment to a specific model. To access all DPS registration features,
|
||||
* including sending custom registration payload and getting the assigned hub, please use the DPS
|
||||
* REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
|
||||
*
|
||||
* <b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a
|
||||
* long time to return. If this is a concern, use the DPS REST API directly, which provides
|
||||
* support for long-running operation status lookup.
|
||||
* @summary Performs DPS registration for a device, optionally assigning it to a model.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
register(deviceId: string, options?: Models.DeviceBridgeRegisterOptionalParams): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
register(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
register(deviceId: string, options: Models.DeviceBridgeRegisterOptionalParams, callback: msRest.ServiceCallback<void>): void;
|
||||
register(deviceId: string, options?: Models.DeviceBridgeRegisterOptionalParams | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
registerOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally it forces the reconnection of the device if it's in a permanent failure state, due
|
||||
* for instance to:
|
||||
* - Bad credentials.
|
||||
* - Device was previously disabled in the cloud side.
|
||||
* - Automatic retries expired (e.g., due to a long period without network connectivity).
|
||||
* @summary Forces a full synchronization of all subscriptions for this device and attempts to
|
||||
* restart any subscriptions in a stopped state.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
resync(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
resync(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
resync(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
|
||||
resync(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
resyncOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets the device twin.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.GetTwinResponse>
|
||||
*/
|
||||
getTwin(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetTwinResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
getTwin(deviceId: string, callback: msRest.ServiceCallback<Models.GetTwinOKResponse>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
getTwin(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.GetTwinOKResponse>): void;
|
||||
getTwin(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.GetTwinOKResponse>, callback?: msRest.ServiceCallback<Models.GetTwinOKResponse>): Promise<Models.GetTwinResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
getTwinOperationSpec,
|
||||
callback) as Promise<Models.GetTwinResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example request:
|
||||
*
|
||||
* PATCH /devices/{deviceId}/properties/reported
|
||||
* {
|
||||
* "patch": {
|
||||
* "fanSpeed": 35,
|
||||
* "serial": "ABC"
|
||||
* }
|
||||
* }
|
||||
* .
|
||||
* @summary Updates reported properties in the device twin.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
updateReportedProperties(deviceId: string, options?: Models.DeviceBridgeUpdateReportedPropertiesOptionalParams): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
updateReportedProperties(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
updateReportedProperties(deviceId: string, options: Models.DeviceBridgeUpdateReportedPropertiesOptionalParams, callback: msRest.ServiceCallback<void>): void;
|
||||
updateReportedProperties(deviceId: string, options?: Models.DeviceBridgeUpdateReportedPropertiesOptionalParams | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
updateReportedPropertiesOperationSpec,
|
||||
callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Gets the current desired property change subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.GetDesiredPropertiesSubscriptionResponse>
|
||||
*/
|
||||
getDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetDesiredPropertiesSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
getDesiredPropertiesSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
getDesiredPropertiesSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
getDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.GetDesiredPropertiesSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
getDesiredPropertiesSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.GetDesiredPropertiesSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the device receives a new desired property change from IoTHub, the service will send an
|
||||
* event to the desired callback URL.
|
||||
*
|
||||
* Example event:
|
||||
* {
|
||||
* "eventType": "string",
|
||||
* "deviceId": "string",
|
||||
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
|
||||
* "desiredProperties": {
|
||||
* "prop1": "string",
|
||||
* "prop2": 12,
|
||||
* "prop3": {},
|
||||
* }
|
||||
* }
|
||||
* .
|
||||
* @summary Creates or updates the current desired property change subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse>
|
||||
*/
|
||||
createOrUpdateDesiredPropertiesSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams): Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateDesiredPropertiesSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
createOrUpdateDesiredPropertiesSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
|
||||
createOrUpdateDesiredPropertiesSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
createOrUpdateDesiredPropertiesSubscriptionOperationSpec,
|
||||
callback) as Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Deletes the current desired property change subscription for a device.
|
||||
* @param deviceId
|
||||
* @param [options] The optional parameters
|
||||
* @returns Promise<msRest.RestResponse>
|
||||
*/
|
||||
deleteDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteDesiredPropertiesSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
|
||||
/**
|
||||
* @param deviceId
|
||||
* @param options The optional parameters
|
||||
* @param callback The callback
|
||||
*/
|
||||
deleteDesiredPropertiesSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
|
||||
deleteDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
|
||||
return this.sendOperationRequest(
|
||||
{
|
||||
deviceId,
|
||||
options
|
||||
},
|
||||
deleteDesiredPropertiesSubscriptionOperationSpec,
|
||||
callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Operation Specifications
|
||||
const serializer = new msRest.Serializer(Mappers);
|
||||
const getCurrentConnectionStatusOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "GET",
|
||||
path: "devices/{deviceId}/ConnectionStatus",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceStatusResponseBody
|
||||
},
|
||||
404: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const getConnectionStatusSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "GET",
|
||||
path: "devices/{deviceId}/ConnectionStatus/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscription
|
||||
},
|
||||
404: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const createOrUpdateConnectionStatusSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "PUT",
|
||||
path: "devices/{deviceId}/ConnectionStatus/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.SubscriptionCreateOrUpdateBody
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscription
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const deleteConnectionStatusSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "DELETE",
|
||||
path: "devices/{deviceId}/ConnectionStatus/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
204: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const getC2DMessageSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "GET",
|
||||
path: "devices/{deviceId}/DeviceBound/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscriptionWithStatus
|
||||
},
|
||||
404: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const createOrUpdateC2DMessageSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "PUT",
|
||||
path: "devices/{deviceId}/DeviceBound/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.SubscriptionCreateOrUpdateBody
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscriptionWithStatus
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const deleteC2DMessageSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "DELETE",
|
||||
path: "devices/{deviceId}/DeviceBound/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
204: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const sendMessageOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "POST",
|
||||
path: "devices/{deviceId}/Messages/events",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.MessageBody
|
||||
},
|
||||
responses: {
|
||||
200: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const getMethodsSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "GET",
|
||||
path: "devices/{deviceId}/Methods/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscriptionWithStatus
|
||||
},
|
||||
404: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const createOrUpdateMethodsSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "PUT",
|
||||
path: "devices/{deviceId}/Methods/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.SubscriptionCreateOrUpdateBody
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscriptionWithStatus
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const deleteMethodsSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "DELETE",
|
||||
path: "devices/{deviceId}/Methods/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
204: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const registerOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "POST",
|
||||
path: "devices/{deviceId}/Registration",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.RegistrationBody
|
||||
},
|
||||
responses: {
|
||||
200: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const resyncOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "POST",
|
||||
path: "devices/{deviceId}/Resync",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
202: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const getTwinOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "GET",
|
||||
path: "devices/{deviceId}/Twin",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.GetTwinOKResponse
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const updateReportedPropertiesOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "PATCH",
|
||||
path: "devices/{deviceId}/Twin/properties/reported",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.ReportedPropertiesPatch
|
||||
},
|
||||
responses: {
|
||||
204: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const getDesiredPropertiesSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "GET",
|
||||
path: "devices/{deviceId}/Twin/properties/desired/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscriptionWithStatus
|
||||
},
|
||||
404: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const createOrUpdateDesiredPropertiesSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "PUT",
|
||||
path: "devices/{deviceId}/Twin/properties/desired/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
requestBody: {
|
||||
parameterPath: [
|
||||
"options",
|
||||
"body"
|
||||
],
|
||||
mapper: Mappers.SubscriptionCreateOrUpdateBody
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
bodyMapper: Mappers.DeviceSubscriptionWithStatus
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
const deleteDesiredPropertiesSubscriptionOperationSpec: msRest.OperationSpec = {
|
||||
httpMethod: "DELETE",
|
||||
path: "devices/{deviceId}/Twin/properties/desired/sub",
|
||||
urlParameters: [
|
||||
Parameters.deviceId
|
||||
],
|
||||
responses: {
|
||||
204: {},
|
||||
default: {}
|
||||
},
|
||||
serializer
|
||||
};
|
||||
|
||||
export {
|
||||
DeviceBridge,
|
||||
DeviceBridgeContext,
|
||||
Models as DeviceBridgeModels,
|
||||
Mappers as DeviceBridgeMappers
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
*/
|
||||
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
import * as Models from "./models";
|
||||
|
||||
const packageName = "";
|
||||
const packageVersion = "";
|
||||
|
||||
export class DeviceBridgeContext extends msRest.ServiceClient {
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the DeviceBridgeContext class.
|
||||
* @param [options] The parameter options
|
||||
*/
|
||||
constructor(options?: Models.DeviceBridgeOptions) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!options.userAgent) {
|
||||
const defaultUserAgent = msRest.getDefaultUserAgentValue();
|
||||
options.userAgent = `${packageName}/${packageVersion} ${defaultUserAgent}`;
|
||||
}
|
||||
|
||||
super(undefined, options);
|
||||
|
||||
this.baseUri = options.baseUri || this.baseUri || "http://localhost";
|
||||
this.requestContentType = "application/json; charset=utf-8";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is regenerated.
|
||||
*/
|
||||
|
||||
import { ServiceClientOptions } from "@azure/ms-rest-js";
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
|
||||
/**
|
||||
* An interface representing DeviceStatusResponseBody.
|
||||
*/
|
||||
export interface DeviceStatusResponseBody {
|
||||
status?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing DeviceSubscription.
|
||||
*/
|
||||
export interface DeviceSubscription {
|
||||
deviceId?: string;
|
||||
subscriptionType?: string;
|
||||
callbackUrl?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing SubscriptionCreateOrUpdateBody.
|
||||
*/
|
||||
export interface SubscriptionCreateOrUpdateBody {
|
||||
callbackUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing DeviceSubscriptionWithStatus.
|
||||
*/
|
||||
export interface DeviceSubscriptionWithStatus {
|
||||
deviceId?: string;
|
||||
subscriptionType?: string;
|
||||
callbackUrl?: string;
|
||||
createdAt?: Date;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing MessageBody.
|
||||
*/
|
||||
export interface MessageBody {
|
||||
data: { [propertyName: string]: any };
|
||||
properties?: { [propertyName: string]: string };
|
||||
componentName?: string;
|
||||
creationTimeUtc?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing RegistrationBody.
|
||||
*/
|
||||
export interface RegistrationBody {
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing ReportedPropertiesPatch.
|
||||
*/
|
||||
export interface ReportedPropertiesPatch {
|
||||
patch: { [propertyName: string]: any };
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing GetTwinOKResponseTwinProperties.
|
||||
*/
|
||||
export interface GetTwinOKResponseTwinProperties {
|
||||
desired?: any;
|
||||
reported?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing GetTwinOKResponseTwin.
|
||||
*/
|
||||
export interface GetTwinOKResponseTwin {
|
||||
properties?: GetTwinOKResponseTwinProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing GetTwinOKResponse.
|
||||
*/
|
||||
export interface GetTwinOKResponse {
|
||||
twin?: GetTwinOKResponseTwin;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing DeviceBridgeOptions.
|
||||
*/
|
||||
export interface DeviceBridgeOptions extends ServiceClientOptions {
|
||||
baseUri?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: SubscriptionCreateOrUpdateBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: SubscriptionCreateOrUpdateBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeSendMessageOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: MessageBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: SubscriptionCreateOrUpdateBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeRegisterOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: RegistrationBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeUpdateReportedPropertiesOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: ReportedPropertiesPatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Parameters.
|
||||
*/
|
||||
export interface DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams extends msRest.RequestOptionsBase {
|
||||
body?: SubscriptionCreateOrUpdateBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains response data for the getCurrentConnectionStatus operation.
|
||||
*/
|
||||
export type GetCurrentConnectionStatusResponse = DeviceStatusResponseBody & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceStatusResponseBody;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the getConnectionStatusSubscription operation.
|
||||
*/
|
||||
export type GetConnectionStatusSubscriptionResponse = DeviceSubscription & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscription;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the createOrUpdateConnectionStatusSubscription operation.
|
||||
*/
|
||||
export type CreateOrUpdateConnectionStatusSubscriptionResponse = DeviceSubscription & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscription;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the getC2DMessageSubscription operation.
|
||||
*/
|
||||
export type GetC2DMessageSubscriptionResponse = DeviceSubscriptionWithStatus & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscriptionWithStatus;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the createOrUpdateC2DMessageSubscription operation.
|
||||
*/
|
||||
export type CreateOrUpdateC2DMessageSubscriptionResponse = DeviceSubscriptionWithStatus & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscriptionWithStatus;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the getMethodsSubscription operation.
|
||||
*/
|
||||
export type GetMethodsSubscriptionResponse = DeviceSubscriptionWithStatus & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscriptionWithStatus;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the createOrUpdateMethodsSubscription operation.
|
||||
*/
|
||||
export type CreateOrUpdateMethodsSubscriptionResponse = DeviceSubscriptionWithStatus & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscriptionWithStatus;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the getTwin operation.
|
||||
*/
|
||||
export type GetTwinResponse = GetTwinOKResponse & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: GetTwinOKResponse;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the getDesiredPropertiesSubscription operation.
|
||||
*/
|
||||
export type GetDesiredPropertiesSubscriptionResponse = DeviceSubscriptionWithStatus & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscriptionWithStatus;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains response data for the createOrUpdateDesiredPropertiesSubscription operation.
|
||||
*/
|
||||
export type CreateOrUpdateDesiredPropertiesSubscriptionResponse = DeviceSubscriptionWithStatus & {
|
||||
/**
|
||||
* The underlying HTTP response.
|
||||
*/
|
||||
_response: msRest.HttpResponse & {
|
||||
/**
|
||||
* The response body as text (string format)
|
||||
*/
|
||||
bodyAsText: string;
|
||||
|
||||
/**
|
||||
* The response body as parsed JSON or XML
|
||||
*/
|
||||
parsedBody: DeviceSubscriptionWithStatus;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is regenerated.
|
||||
*/
|
||||
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
|
||||
|
||||
export const DeviceStatusResponseBody: msRest.CompositeMapper = {
|
||||
serializedName: "DeviceStatusResponseBody",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "DeviceStatusResponseBody",
|
||||
modelProperties: {
|
||||
status: {
|
||||
serializedName: "status",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
reason: {
|
||||
serializedName: "reason",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const DeviceSubscription: msRest.CompositeMapper = {
|
||||
serializedName: "DeviceSubscription",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "DeviceSubscription",
|
||||
modelProperties: {
|
||||
deviceId: {
|
||||
serializedName: "deviceId",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
subscriptionType: {
|
||||
serializedName: "subscriptionType",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
callbackUrl: {
|
||||
serializedName: "callbackUrl",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
serializedName: "createdAt",
|
||||
type: {
|
||||
name: "DateTime"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SubscriptionCreateOrUpdateBody: msRest.CompositeMapper = {
|
||||
serializedName: "SubscriptionCreateOrUpdateBody",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "SubscriptionCreateOrUpdateBody",
|
||||
modelProperties: {
|
||||
callbackUrl: {
|
||||
required: true,
|
||||
serializedName: "callbackUrl",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const DeviceSubscriptionWithStatus: msRest.CompositeMapper = {
|
||||
serializedName: "DeviceSubscriptionWithStatus",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "DeviceSubscriptionWithStatus",
|
||||
modelProperties: {
|
||||
deviceId: {
|
||||
serializedName: "deviceId",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
subscriptionType: {
|
||||
serializedName: "subscriptionType",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
callbackUrl: {
|
||||
serializedName: "callbackUrl",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
serializedName: "createdAt",
|
||||
type: {
|
||||
name: "DateTime"
|
||||
}
|
||||
},
|
||||
status: {
|
||||
serializedName: "status",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const MessageBody: msRest.CompositeMapper = {
|
||||
serializedName: "MessageBody",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "MessageBody",
|
||||
modelProperties: {
|
||||
data: {
|
||||
required: true,
|
||||
serializedName: "data",
|
||||
type: {
|
||||
name: "Dictionary",
|
||||
value: {
|
||||
type: {
|
||||
name: "Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
serializedName: "properties",
|
||||
type: {
|
||||
name: "Dictionary",
|
||||
value: {
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
componentName: {
|
||||
serializedName: "componentName",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
},
|
||||
creationTimeUtc: {
|
||||
serializedName: "creationTimeUtc",
|
||||
type: {
|
||||
name: "DateTime"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const RegistrationBody: msRest.CompositeMapper = {
|
||||
serializedName: "RegistrationBody",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "RegistrationBody",
|
||||
modelProperties: {
|
||||
modelId: {
|
||||
serializedName: "modelId",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ReportedPropertiesPatch: msRest.CompositeMapper = {
|
||||
serializedName: "ReportedPropertiesPatch",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "ReportedPropertiesPatch",
|
||||
modelProperties: {
|
||||
patch: {
|
||||
required: true,
|
||||
serializedName: "patch",
|
||||
type: {
|
||||
name: "Dictionary",
|
||||
value: {
|
||||
type: {
|
||||
name: "Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GetTwinOKResponseTwinProperties: msRest.CompositeMapper = {
|
||||
serializedName: "GetTwinOKResponse_twin_properties",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "GetTwinOKResponseTwinProperties",
|
||||
modelProperties: {
|
||||
desired: {
|
||||
serializedName: "desired",
|
||||
type: {
|
||||
name: "Object"
|
||||
}
|
||||
},
|
||||
reported: {
|
||||
serializedName: "reported",
|
||||
type: {
|
||||
name: "Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GetTwinOKResponseTwin: msRest.CompositeMapper = {
|
||||
serializedName: "GetTwinOKResponse_twin",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "GetTwinOKResponseTwin",
|
||||
modelProperties: {
|
||||
properties: {
|
||||
serializedName: "properties",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "GetTwinOKResponseTwinProperties"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GetTwinOKResponse: msRest.CompositeMapper = {
|
||||
serializedName: "GetTwinOKResponse",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "GetTwinOKResponse",
|
||||
modelProperties: {
|
||||
twin: {
|
||||
serializedName: "twin",
|
||||
type: {
|
||||
name: "Composite",
|
||||
className: "GetTwinOKResponseTwin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Code generated by Microsoft (R) AutoRest Code Generator.
|
||||
* Changes may cause incorrect behavior and will be lost if the code is
|
||||
* regenerated.
|
||||
*/
|
||||
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
|
||||
export const deviceId: msRest.OperationURLParameter = {
|
||||
parameterPath: "deviceId",
|
||||
mapper: {
|
||||
required: true,
|
||||
serializedName: "deviceId",
|
||||
type: {
|
||||
name: "String"
|
||||
}
|
||||
}
|
||||
};
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче