Save simulations in the storage (#32)
* Store simulations using the storage adapter * Implement device bootstrap scenario: when a new device is created, mark it as a simulated device and report some properties like device type, messages schema, and initial location. * Remove dependency on IoT Hub manager and access IoT Hub directly * Refactor state machine to reduce complexity and reuse code * Add launch settings for Visual Studio * Remove env var used for the web service TCP port * Improve logging of exceptions to avoid log flooding * Fix messages format, to always use the “_unit” convention * Add JSON config checks
This commit is contained in:
Родитель
abbd305492
Коммит
f2c8d6e92d
|
@ -6,7 +6,9 @@ using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
|||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using Services.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
@ -19,19 +21,21 @@ namespace Services.Test
|
|||
/// <summary>The test logger</summary>
|
||||
private readonly ITestOutputHelper log;
|
||||
|
||||
private const string StorageCollection = "simulations";
|
||||
private const string SimulationId = "1";
|
||||
|
||||
private readonly Mock<IDeviceTypes> deviceTypes;
|
||||
private readonly Mock<IStorageAdapterClient> storage;
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Simulations target;
|
||||
private readonly List<DeviceType> types;
|
||||
private readonly Mock<ILogger> logger;
|
||||
|
||||
public SimulationsTest(ITestOutputHelper log)
|
||||
{
|
||||
this.log = log;
|
||||
|
||||
var tempStorage = Guid.NewGuid() + ".json";
|
||||
this.log.WriteLine("Temporary simulations storage: " + tempStorage);
|
||||
|
||||
this.deviceTypes = new Mock<IDeviceTypes>();
|
||||
this.storage = new Mock<IStorageAdapterClient>();
|
||||
this.logger = new Mock<ILogger>();
|
||||
|
||||
this.types = new List<DeviceType>
|
||||
|
@ -42,15 +46,17 @@ namespace Services.Test
|
|||
new DeviceType { Id = "AA" }
|
||||
};
|
||||
|
||||
this.target = new Simulations(this.deviceTypes.Object, this.logger.Object);
|
||||
this.target.ChangeStorageFile(tempStorage);
|
||||
this.target = new Simulations(this.deviceTypes.Object, this.storage.Object, this.logger.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.Type, Constants.UnitTest)]
|
||||
public void InitialListIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
|
||||
// Act
|
||||
IList<SimulationModel> list = this.target.GetList();
|
||||
var list = this.target.GetListAsync().Result;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, list.Count);
|
||||
|
@ -60,10 +66,11 @@ namespace Services.Test
|
|||
public void InitialMetadataAfterCreation()
|
||||
{
|
||||
// Arrange
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
|
||||
// Act
|
||||
SimulationModel result = this.target.Insert(new SimulationModel(), "default");
|
||||
SimulationModel result = this.target.InsertAsync(new SimulationModel(), "default").Result;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, result.Version);
|
||||
|
@ -75,10 +82,11 @@ namespace Services.Test
|
|||
{
|
||||
// Arrange
|
||||
const int defaultDeviceCount = 2;
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
|
||||
// Act
|
||||
SimulationModel result = this.target.Insert(new SimulationModel(), "default");
|
||||
SimulationModel result = this.target.InsertAsync(new SimulationModel(), "default").Result;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(this.types.Count, result.DeviceTypes.Count);
|
||||
|
@ -93,10 +101,11 @@ namespace Services.Test
|
|||
public void CreateSimulationWithoutId()
|
||||
{
|
||||
// Arrange
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
|
||||
// Act
|
||||
SimulationModel result = this.target.Insert(new SimulationModel(), "default");
|
||||
SimulationModel result = this.target.InsertAsync(new SimulationModel(), "default").Result;
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(result.Id);
|
||||
|
@ -106,11 +115,12 @@ namespace Services.Test
|
|||
public void CreateSimulationWithId()
|
||||
{
|
||||
// Arrange
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
|
||||
// Act
|
||||
var simulation = new SimulationModel { Id = "123" };
|
||||
SimulationModel result = this.target.Insert(simulation, "default");
|
||||
SimulationModel result = this.target.InsertAsync(simulation, "default").Result;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(simulation.Id, result.Id);
|
||||
|
@ -120,52 +130,58 @@ namespace Services.Test
|
|||
public void CreateWithInvalidTemplate()
|
||||
{
|
||||
// Act + Assert
|
||||
Assert.Throws<InvalidInputException>(() => this.target.Insert(new SimulationModel(), "foo"));
|
||||
Assert.ThrowsAsync<InvalidInputException>(
|
||||
() => this.target.InsertAsync(new SimulationModel(), "mytemplate"));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.Type, Constants.UnitTest)]
|
||||
public void CreatingMultipleSimulationsIsNotAllowed()
|
||||
{
|
||||
// Arrange
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.target.Insert(new SimulationModel(), "default");
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
this.ThereIsAnEnabledSimulationInTheStorage();
|
||||
|
||||
// Act + Assert
|
||||
var s = new SimulationModel { Id = Guid.NewGuid().ToString(), Enabled = false };
|
||||
Assert.Throws<ConflictingResourceException>(() => this.target.Insert(s));
|
||||
Assert.Throws<ConflictingResourceException>(() => this.target.Upsert(s));
|
||||
Assert.ThrowsAsync<ConflictingResourceException>(() => this.target.InsertAsync(s));
|
||||
Assert.ThrowsAsync<ConflictingResourceException>(() => this.target.UpsertAsync(s));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.Type, Constants.UnitTest)]
|
||||
public void CreatedSimulationsAreStored()
|
||||
{
|
||||
// Arrange
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
|
||||
// Act
|
||||
var simulation = new SimulationModel { Id = Guid.NewGuid().ToString(), Enabled = false };
|
||||
this.target.Insert(simulation, "default");
|
||||
var result = this.target.Get(simulation.Id);
|
||||
this.target.InsertAsync(simulation, "default").Wait();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(simulation.Id, result.Id);
|
||||
Assert.Equal(simulation.Enabled, result.Enabled);
|
||||
this.storage.Verify(
|
||||
x => x.UpdateAsync(StorageCollection, SimulationId, It.IsAny<string>(), "*"));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.Type, Constants.UnitTest)]
|
||||
public void SimulationsCanBeUpserted()
|
||||
{
|
||||
// Arrange
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
this.ThereAreSomeDeviceTypes();
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
|
||||
// Act
|
||||
var simulation = new SimulationModel { Id = Guid.NewGuid().ToString(), Enabled = false };
|
||||
this.target.Upsert(simulation, "default");
|
||||
var result = this.target.Get(simulation.Id);
|
||||
var simulation = new SimulationModel
|
||||
{
|
||||
Id = SimulationId,
|
||||
Enabled = false,
|
||||
Etag = "2345213461"
|
||||
};
|
||||
this.target.UpsertAsync(simulation).Wait();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(simulation.Id, result.Id);
|
||||
Assert.Equal(simulation.Enabled, result.Enabled);
|
||||
this.storage.Verify(
|
||||
x => x.UpdateAsync(StorageCollection, SimulationId, It.IsAny<string>(), simulation.Etag));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.Type, Constants.UnitTest)]
|
||||
|
@ -174,10 +190,10 @@ namespace Services.Test
|
|||
// Act
|
||||
var s1 = new SimulationModel();
|
||||
var s2 = new SimulationModel();
|
||||
this.target.Insert(s1);
|
||||
this.target.InsertAsync(s1);
|
||||
|
||||
// Act + Assert
|
||||
Assert.Throws<InvalidInputException>(() => this.target.Upsert(s2));
|
||||
Assert.ThrowsAsync<InvalidInputException>(() => this.target.UpsertAsync(s2));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.Type, Constants.UnitTest)]
|
||||
|
@ -188,11 +204,45 @@ namespace Services.Test
|
|||
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var s1 = new SimulationModel { Id = id, Enabled = false };
|
||||
this.target.Upsert(s1);
|
||||
this.target.UpsertAsync(s1);
|
||||
|
||||
// Act + Assert
|
||||
var s1updated = new SimulationModel { Id = id, Enabled = true };
|
||||
Assert.Throws<ResourceOutOfDateException>(() => this.target.Upsert(s1updated));
|
||||
Assert.ThrowsAsync<ResourceOutOfDateException>(() => this.target.UpsertAsync(s1updated));
|
||||
}
|
||||
|
||||
private void ThereAreSomeDeviceTypes()
|
||||
{
|
||||
this.deviceTypes.Setup(x => x.GetList()).Returns(this.types);
|
||||
}
|
||||
|
||||
private void ThereAreNoSimulationsInTheStorage()
|
||||
{
|
||||
this.storage.Setup(x => x.GetAllAsync(StorageCollection)).ReturnsAsync(new ValueListApiModel());
|
||||
}
|
||||
|
||||
private void ThereIsAnEnabledSimulationInTheStorage()
|
||||
{
|
||||
var simulation = new SimulationModel
|
||||
{
|
||||
Id = SimulationId,
|
||||
Created = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(10)),
|
||||
Modified = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(10)),
|
||||
Etag = "etag0",
|
||||
Enabled = true,
|
||||
Version = 1
|
||||
};
|
||||
|
||||
var list = new ValueListApiModel();
|
||||
var value = new ValueApiModel
|
||||
{
|
||||
Key = SimulationId,
|
||||
Data = JsonConvert.SerializeObject(simulation),
|
||||
ETag = simulation.Etag
|
||||
};
|
||||
list.Items.Add(value);
|
||||
|
||||
this.storage.Setup(x => x.GetAllAsync(StorageCollection)).ReturnsAsync(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,8 +63,15 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
|
||||
public void Stop()
|
||||
{
|
||||
this.timer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
this.timer?.Dispose();
|
||||
try
|
||||
{
|
||||
this.timer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
this.timer?.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException e)
|
||||
{
|
||||
this.log.Info("The timer was already disposed.", () => new { e });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
private readonly string ioTHubHostName;
|
||||
|
||||
public Devices(
|
||||
ILogger logger,
|
||||
IServicesConfig config)
|
||||
IServicesConfig config,
|
||||
ILogger logger)
|
||||
{
|
||||
this.log = logger;
|
||||
this.registry = RegistryManager.CreateFromConnectionString(config.IoTHubConnString);
|
||||
|
|
|
@ -23,6 +23,11 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions
|
|||
{
|
||||
}
|
||||
|
||||
public ExternalDependencyException(Exception innerException)
|
||||
: base("An unexpected error happened while using an external dependency.", innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ExternalDependencyException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
|
|
|
@ -96,7 +96,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
{
|
||||
StatusCode = response.StatusCode,
|
||||
Headers = response.Headers,
|
||||
Content = await response.Content.ReadAsStringAsync(),
|
||||
Content = await response.Content.ReadAsStringAsync()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -108,13 +108,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
errorMessage += " - " + e.InnerException.Message;
|
||||
}
|
||||
|
||||
this.log.Error("Request failed", () => new
|
||||
{
|
||||
ExceptionMessage = e.Message,
|
||||
InnerExceptionType = e.InnerException != null ? e.InnerException.GetType().FullName : "",
|
||||
InnerExceptionMessage = e.InnerException != null ? e.InnerException.Message : "",
|
||||
errorMessage
|
||||
});
|
||||
this.log.Error("Request failed", () => new { errorMessage, e });
|
||||
|
||||
return new HttpResponse
|
||||
{
|
||||
|
@ -122,6 +116,26 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
Content = errorMessage
|
||||
};
|
||||
}
|
||||
catch (TaskCanceledException e)
|
||||
{
|
||||
this.log.Error("Request failed", () => new { Message = e.Message + " The request timed out, the endpoint might be unreachable.", e });
|
||||
|
||||
return new HttpResponse
|
||||
{
|
||||
StatusCode = 0,
|
||||
Content = e.Message + " The endpoint might be unreachable."
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Request failed", () => new { e.Message, e });
|
||||
|
||||
return new HttpResponse
|
||||
{
|
||||
StatusCode = 0,
|
||||
Content = e.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,27 +20,27 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
|
||||
HttpContent Content { get; }
|
||||
|
||||
void AddHeader(string name, string value);
|
||||
IHttpRequest AddHeader(string name, string value);
|
||||
|
||||
void SetUriFromString(string uri);
|
||||
IHttpRequest SetUriFromString(string uri);
|
||||
|
||||
void SetContent(string content);
|
||||
IHttpRequest SetContent(string content);
|
||||
|
||||
void SetContent(string content, Encoding encoding);
|
||||
IHttpRequest SetContent(string content, Encoding encoding);
|
||||
|
||||
void SetContent(string content, Encoding encoding, string mediaType);
|
||||
IHttpRequest SetContent(string content, Encoding encoding, string mediaType);
|
||||
|
||||
void SetContent(string content, Encoding encoding, MediaTypeHeaderValue mediaType);
|
||||
IHttpRequest SetContent(string content, Encoding encoding, MediaTypeHeaderValue mediaType);
|
||||
|
||||
void SetContent(StringContent stringContent);
|
||||
IHttpRequest SetContent(StringContent stringContent);
|
||||
|
||||
void SetContent<T>(T sourceObject);
|
||||
IHttpRequest SetContent<T>(T sourceObject);
|
||||
|
||||
void SetContent<T>(T sourceObject, Encoding encoding);
|
||||
IHttpRequest SetContent<T>(T sourceObject, Encoding encoding);
|
||||
|
||||
void SetContent<T>(T sourceObject, Encoding encoding, string mediaType);
|
||||
IHttpRequest SetContent<T>(T sourceObject, Encoding encoding, string mediaType);
|
||||
|
||||
void SetContent<T>(T sourceObject, Encoding encoding, MediaTypeHeaderValue mediaType);
|
||||
IHttpRequest SetContent<T>(T sourceObject, Encoding encoding, MediaTypeHeaderValue mediaType);
|
||||
}
|
||||
|
||||
public class HttpRequest : IHttpRequest
|
||||
|
@ -76,7 +76,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
this.SetUriFromString(uri);
|
||||
}
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
public IHttpRequest AddHeader(string name, string value)
|
||||
{
|
||||
if (!this.Headers.TryAddWithoutValidation(name, value))
|
||||
{
|
||||
|
@ -87,60 +87,66 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
|
||||
this.ContentType = new MediaTypeHeaderValue(value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void SetUriFromString(string uri)
|
||||
public IHttpRequest SetUriFromString(string uri)
|
||||
{
|
||||
this.Uri = new Uri(uri);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void SetContent(string content)
|
||||
public IHttpRequest SetContent(string content)
|
||||
{
|
||||
this.SetContent(content, this.defaultEncoding, this.defaultMediaType);
|
||||
return this.SetContent(content, this.defaultEncoding, this.defaultMediaType);
|
||||
}
|
||||
|
||||
public void SetContent(string content, Encoding encoding)
|
||||
public IHttpRequest SetContent(string content, Encoding encoding)
|
||||
{
|
||||
this.SetContent(content, encoding, this.defaultMediaType);
|
||||
return this.SetContent(content, encoding, this.defaultMediaType);
|
||||
}
|
||||
|
||||
public void SetContent(string content, Encoding encoding, string mediaType)
|
||||
public IHttpRequest SetContent(string content, Encoding encoding, string mediaType)
|
||||
{
|
||||
this.SetContent(content, encoding, new MediaTypeHeaderValue(mediaType));
|
||||
return this.SetContent(content, encoding, new MediaTypeHeaderValue(mediaType));
|
||||
}
|
||||
|
||||
public void SetContent(string content, Encoding encoding, MediaTypeHeaderValue mediaType)
|
||||
public IHttpRequest SetContent(string content, Encoding encoding, MediaTypeHeaderValue mediaType)
|
||||
{
|
||||
this.requestContent.Content = new StringContent(content, encoding, mediaType.MediaType);
|
||||
this.ContentType = mediaType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void SetContent(StringContent stringContent)
|
||||
public IHttpRequest SetContent(StringContent stringContent)
|
||||
{
|
||||
this.requestContent.Content = stringContent;
|
||||
this.ContentType = stringContent.Headers.ContentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void SetContent<T>(T sourceObject)
|
||||
public IHttpRequest SetContent<T>(T sourceObject)
|
||||
{
|
||||
this.SetContent(sourceObject, this.defaultEncoding, this.defaultMediaType);
|
||||
return this.SetContent(sourceObject, this.defaultEncoding, this.defaultMediaType);
|
||||
}
|
||||
|
||||
public void SetContent<T>(T sourceObject, Encoding encoding)
|
||||
public IHttpRequest SetContent<T>(T sourceObject, Encoding encoding)
|
||||
{
|
||||
this.SetContent(sourceObject, encoding, this.defaultMediaType);
|
||||
return this.SetContent(sourceObject, encoding, this.defaultMediaType);
|
||||
}
|
||||
|
||||
public void SetContent<T>(T sourceObject, Encoding encoding, string mediaType)
|
||||
public IHttpRequest SetContent<T>(T sourceObject, Encoding encoding, string mediaType)
|
||||
{
|
||||
this.SetContent(sourceObject, encoding, new MediaTypeHeaderValue(mediaType));
|
||||
return this.SetContent(sourceObject, encoding, new MediaTypeHeaderValue(mediaType));
|
||||
}
|
||||
|
||||
public void SetContent<T>(T sourceObject, Encoding encoding, MediaTypeHeaderValue mediaType)
|
||||
public IHttpRequest SetContent<T>(T sourceObject, Encoding encoding, MediaTypeHeaderValue mediaType)
|
||||
{
|
||||
var content = JsonConvert.SerializeObject(sourceObject, Formatting.None);
|
||||
this.requestContent.Content = new StringContent(content, encoding, mediaType.MediaType);
|
||||
this.ContentType = mediaType;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,20 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
HttpStatusCode StatusCode { get; }
|
||||
HttpResponseHeaders Headers { get; }
|
||||
string Content { get; }
|
||||
|
||||
bool IsSuccess { get; }
|
||||
bool IsError { get; }
|
||||
bool IsIncomplete { get; }
|
||||
bool IsNonRetriableError { get; }
|
||||
bool IsRetriableError { get; }
|
||||
bool IsBadRequest { get; }
|
||||
bool IsUnauthorized { get; }
|
||||
bool IsForbidden { get; }
|
||||
bool IsNotFound { get; }
|
||||
bool IsTimeout { get; }
|
||||
bool IsConflict { get; }
|
||||
bool IsServerError { get; }
|
||||
bool IsServiceUnavailable { get; }
|
||||
}
|
||||
|
||||
public class HttpResponse : IHttpResponse
|
||||
|
@ -35,8 +48,31 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
public HttpResponseHeaders Headers { get; internal set; }
|
||||
public string Content { get; internal set; }
|
||||
|
||||
public bool IsSuccess => (int) this.StatusCode >= 200 && (int) this.StatusCode <= 299;
|
||||
public bool IsError => (int) this.StatusCode >= 400 || (int) this.StatusCode == 0;
|
||||
|
||||
public bool IsIncomplete
|
||||
{
|
||||
get
|
||||
{
|
||||
var c = (int) this.StatusCode;
|
||||
return (c >= 100 && c <= 199) || (c >= 300 && c <= 399);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNonRetriableError => IsError && !IsRetriableError;
|
||||
|
||||
public bool IsRetriableError => this.StatusCode == HttpStatusCode.NotFound ||
|
||||
this.StatusCode == HttpStatusCode.RequestTimeout ||
|
||||
(int) this.StatusCode == TooManyRequests;
|
||||
|
||||
public bool IsBadRequest => (int) this.StatusCode == 400;
|
||||
public bool IsUnauthorized => (int) this.StatusCode == 401;
|
||||
public bool IsForbidden => (int) this.StatusCode == 403;
|
||||
public bool IsNotFound => (int) this.StatusCode == 404;
|
||||
public bool IsTimeout => (int) this.StatusCode == 408;
|
||||
public bool IsConflict => (int) this.StatusCode == 409;
|
||||
public bool IsServerError => (int) this.StatusCode >= 500;
|
||||
public bool IsServiceUnavailable => (int) this.StatusCode == 503;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
Service layer
|
||||
=============
|
||||
|
||||
## Guidelines
|
||||
|
||||
* The service layer is responsible for the business logic of the microservice
|
||||
and for dealing with external dependencies like storage, IoT Hub, etc.
|
||||
* The service layer has no knowledge of the web service or other entry points.
|
||||
|
||||
## Conventions
|
||||
|
||||
* Configuration is injected into the service layer by the entry point projects.
|
|
@ -9,6 +9,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
|||
string DeviceTypesFolder { get; set; }
|
||||
string DeviceTypesScriptsFolder { get; set; }
|
||||
string IoTHubConnString { get; set; }
|
||||
string StorageAdapterApiUrl { get; set; }
|
||||
int StorageAdapterApiTimeout { get; set; }
|
||||
}
|
||||
|
||||
// TODO: test Windows/Linux folder separator
|
||||
|
@ -31,6 +33,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
|||
|
||||
public string IoTHubConnString { get; set; }
|
||||
|
||||
public string StorageAdapterApiUrl { get; set; }
|
||||
|
||||
public int StorageAdapterApiTimeout { get; set; }
|
||||
|
||||
private string NormalizePath(string path)
|
||||
{
|
||||
return path
|
||||
|
|
|
@ -2,78 +2,67 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// TODO: use real storage
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public interface ISimulations
|
||||
{
|
||||
IList<Models.Simulation> GetList();
|
||||
Models.Simulation Get(string id);
|
||||
Models.Simulation Insert(Models.Simulation simulation, string template = "");
|
||||
Models.Simulation Upsert(Models.Simulation simulation, string template = "");
|
||||
Models.Simulation Merge(SimulationPatch patch);
|
||||
Task<IList<Models.Simulation>> GetListAsync();
|
||||
Task<Models.Simulation> GetAsync(string id);
|
||||
Task<Models.Simulation> InsertAsync(Models.Simulation simulation, string template = "");
|
||||
Task<Models.Simulation> UpsertAsync(Models.Simulation simulation);
|
||||
Task<Models.Simulation> MergeAsync(SimulationPatch patch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Since we don't have a configuration service yet, data is
|
||||
/// stored to a JSON file.
|
||||
/// </summary>
|
||||
public class Simulations : ISimulations
|
||||
{
|
||||
private string tempStorageFile = "simulations-storage.json";
|
||||
private string tempStoragePath;
|
||||
private const string StorageCollection = "simulations";
|
||||
private const string SimulationId = "1";
|
||||
private const int DevicesPerTypeInDefaultTemplate = 2;
|
||||
|
||||
private readonly IDeviceTypes deviceTypes;
|
||||
private readonly IStorageAdapterClient storage;
|
||||
private readonly ILogger log;
|
||||
|
||||
public Simulations(
|
||||
IDeviceTypes deviceTypes,
|
||||
IStorageAdapterClient storage,
|
||||
ILogger logger)
|
||||
{
|
||||
this.deviceTypes = deviceTypes;
|
||||
this.storage = storage;
|
||||
this.log = logger;
|
||||
|
||||
this.SetupTempStoragePath();
|
||||
this.CreateStorageIfMissing();
|
||||
}
|
||||
|
||||
// TODO: remove this method and use mocks when we have a storage service
|
||||
public void ChangeStorageFile(string filename)
|
||||
public async Task<IList<Models.Simulation>> GetListAsync()
|
||||
{
|
||||
this.tempStorageFile = filename;
|
||||
this.SetupTempStoragePath();
|
||||
this.CreateStorageIfMissing();
|
||||
}
|
||||
|
||||
public IList<Models.Simulation> GetList()
|
||||
{
|
||||
this.CreateStorageIfMissing();
|
||||
var json = File.ReadAllText(this.tempStoragePath);
|
||||
return JsonConvert.DeserializeObject<List<Models.Simulation>>(json);
|
||||
}
|
||||
|
||||
public Models.Simulation Get(string id)
|
||||
{
|
||||
var simulations = this.GetList();
|
||||
foreach (var s in simulations)
|
||||
var data = await this.storage.GetAllAsync(StorageCollection);
|
||||
var result = new List<Models.Simulation>();
|
||||
foreach (var item in data.Items)
|
||||
{
|
||||
if (s.Id == id) return s;
|
||||
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
|
||||
simulation.Etag = item.ETag;
|
||||
result.Add(simulation);
|
||||
}
|
||||
|
||||
this.log.Warn("Simulation not found", () => new { id });
|
||||
|
||||
throw new ResourceNotFoundException();
|
||||
return result;
|
||||
}
|
||||
|
||||
public Models.Simulation Insert(Models.Simulation simulation, string template = "")
|
||||
public async Task<Models.Simulation> GetAsync(string id)
|
||||
{
|
||||
var item = await this.storage.GetAsync(StorageCollection, id);
|
||||
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
|
||||
simulation.Etag = item.ETag;
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public async Task<Models.Simulation> InsertAsync(Models.Simulation simulation, string template = "")
|
||||
{
|
||||
// TODO: complete validation
|
||||
if (!string.IsNullOrEmpty(template) && template.ToLowerInvariant() != "default")
|
||||
|
@ -82,24 +71,16 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
throw new InvalidInputException("Unknown template name. Try 'default'.");
|
||||
}
|
||||
|
||||
var simulations = this.GetList();
|
||||
|
||||
// Only one simulation per deployment
|
||||
var simulations = await this.GetListAsync();
|
||||
if (simulations.Count > 0)
|
||||
{
|
||||
this.log.Warn("There is already a simulation",
|
||||
() => new { Existing = simulations.First().Id });
|
||||
|
||||
this.log.Warn("There is already a simulation", () => { });
|
||||
throw new ConflictingResourceException(
|
||||
"There is already a simulation. Only one simulation can be created.");
|
||||
}
|
||||
|
||||
// The ID is not empty when using PUT
|
||||
if (string.IsNullOrEmpty(simulation.Id))
|
||||
{
|
||||
simulation.Id = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
// Note: forcing the ID because only one simulation can be created
|
||||
simulation.Id = SimulationId;
|
||||
simulation.Created = DateTimeOffset.UtcNow;
|
||||
simulation.Modified = simulation.Created;
|
||||
simulation.Version = 1;
|
||||
|
@ -114,136 +95,97 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
simulation.DeviceTypes.Add(new Models.Simulation.DeviceTypeRef
|
||||
{
|
||||
Id = type.Id,
|
||||
Count = 2
|
||||
Count = DevicesPerTypeInDefaultTemplate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.WriteToStorage(simulation);
|
||||
// Note: using UpdateAsync because the service generates the ID
|
||||
await this.storage.UpdateAsync(
|
||||
StorageCollection,
|
||||
SimulationId,
|
||||
JsonConvert.SerializeObject(simulation),
|
||||
"*");
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public Models.Simulation Upsert(Models.Simulation simulation, string template = "")
|
||||
{
|
||||
// TODO: complete validation
|
||||
if (string.IsNullOrEmpty(simulation.Id))
|
||||
{
|
||||
this.log.Warn("Missing ID", () => new { simulation });
|
||||
throw new InvalidInputException("Missing ID");
|
||||
}
|
||||
|
||||
var simulations = this.GetList();
|
||||
if (simulations.Count == 0)
|
||||
{
|
||||
return this.Insert(simulation, template);
|
||||
}
|
||||
|
||||
// Note: only one simulation per deployment
|
||||
if (simulations[0].Id != simulation.Id)
|
||||
{
|
||||
this.log.Warn("There is already a simulation",
|
||||
() => new { Existing = simulations[0].Id, Provided = simulation.Id });
|
||||
throw new ConflictingResourceException(
|
||||
"There is already a simulation. Only one simulation can be created.");
|
||||
}
|
||||
|
||||
if (simulations[0].Etag != simulation.Etag)
|
||||
{
|
||||
this.log.Warn("Etag mismatch",
|
||||
() => new { Existing = simulations[0].Etag, Provided = simulation.Etag });
|
||||
throw new ResourceOutOfDateException(
|
||||
"Etag mismatch: the resource has been updated by another client.");
|
||||
}
|
||||
|
||||
simulation.Modified = DateTimeOffset.UtcNow;
|
||||
simulation.Version += 1;
|
||||
|
||||
this.WriteToStorage(simulation);
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public Models.Simulation Merge(SimulationPatch patch)
|
||||
{
|
||||
var simulations = this.GetList();
|
||||
|
||||
if (simulations.Count == 0 || simulations[0].Id != patch.Id)
|
||||
{
|
||||
this.log.Warn("The simulation doesn't exist.",
|
||||
() => new { ExistingSimulations = simulations.Count, IdProvided = patch.Id });
|
||||
throw new ResourceNotFoundException("The simulation doesn't exist.");
|
||||
}
|
||||
|
||||
if (simulations[0].Etag != patch.Etag)
|
||||
{
|
||||
this.log.Warn("Etag mismatch",
|
||||
() => new { Existing = simulations[0].Etag, Provided = patch.Etag });
|
||||
throw new ResourceOutOfDateException(
|
||||
"Etag mismatch: the resource has been updated by another client.");
|
||||
}
|
||||
|
||||
var simulation = simulations[0];
|
||||
|
||||
var resourceChanged = false;
|
||||
if (patch.Enabled.HasValue && patch.Enabled.Value != simulation.Enabled)
|
||||
{
|
||||
simulation.Enabled = patch.Enabled.Value;
|
||||
resourceChanged = true;
|
||||
}
|
||||
|
||||
if (resourceChanged)
|
||||
{
|
||||
simulation.Modified = DateTimeOffset.UtcNow;
|
||||
simulation.Version += 1;
|
||||
this.WriteToStorage(simulation);
|
||||
}
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
private void WriteToStorage(Models.Simulation simulation)
|
||||
{
|
||||
simulation.Etag = Etags.NewEtag();
|
||||
var data = new List<Models.Simulation> { simulation };
|
||||
File.WriteAllText(this.tempStoragePath, JsonConvert.SerializeObject(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// While working within an IDE we need to share the data used by the
|
||||
/// web service and the data used by the simulation agent, i.e. when
|
||||
/// running the web service and the simulation agent from the IDE there
|
||||
/// are two "Services/data" folders, one in each entry point. Thus the
|
||||
/// simulation agent would not see the temporary storage written by the
|
||||
/// web service. By using the user/system temp folder, we make sure the
|
||||
/// storage is shared by the two processes when using an IDE.
|
||||
/// Upsert the simulation. The logic works under the assumption that
|
||||
/// there is only one simulation with id "1".
|
||||
/// </summary>
|
||||
private void SetupTempStoragePath()
|
||||
public async Task<Models.Simulation> UpsertAsync(Models.Simulation simulation)
|
||||
{
|
||||
var tempFolder = Path.GetTempPath();
|
||||
|
||||
// In some cases GetTempPath returns a path under "/var/folders/"
|
||||
// in which case we opt for /tmp/. Note: this is temporary until
|
||||
// we use a real storage service.
|
||||
if (Path.DirectorySeparatorChar == '/' && Directory.Exists("/tmp/"))
|
||||
if (simulation.Id != SimulationId)
|
||||
{
|
||||
tempFolder = "/tmp/";
|
||||
this.log.Warn("Invalid simulation ID. Only one simulation is allowed", () => { });
|
||||
throw new InvalidInputException("Invalid simulation ID. Use ID '" + SimulationId + "'.");
|
||||
}
|
||||
|
||||
this.tempStoragePath = (tempFolder + Path.DirectorySeparatorChar + this.tempStorageFile)
|
||||
.Replace(Path.DirectorySeparatorChar.ToString() + Path.DirectorySeparatorChar,
|
||||
Path.DirectorySeparatorChar.ToString());
|
||||
var simulations = await this.GetListAsync();
|
||||
if (simulations.Count > 0)
|
||||
{
|
||||
//simulation.Modified = DateTimeOffset.UtcNow;
|
||||
//simulation.Version = simulations.First().Version + 1;
|
||||
this.log.Error("Simulations cannot be modified via PUT. Use PATCH to start/stop the simulation.", () => { });
|
||||
throw new InvalidInputException("Simulations cannot be modified via PUT. Use PATCH to start/stop the simulation.");
|
||||
}
|
||||
|
||||
this.log.Info("Temporary simulations storage: " + this.tempStoragePath, () => { });
|
||||
// Note: forcing the ID because only one simulation can be created
|
||||
simulation.Id = SimulationId;
|
||||
simulation.Created = DateTimeOffset.UtcNow;
|
||||
simulation.Modified = simulation.Created;
|
||||
simulation.Version = 1;
|
||||
|
||||
await this.storage.UpdateAsync(
|
||||
StorageCollection,
|
||||
SimulationId,
|
||||
JsonConvert.SerializeObject(simulation),
|
||||
simulation.Etag);
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
private void CreateStorageIfMissing()
|
||||
public async Task<Models.Simulation> MergeAsync(SimulationPatch patch)
|
||||
{
|
||||
if (!File.Exists(this.tempStoragePath))
|
||||
if (patch.Id != SimulationId)
|
||||
{
|
||||
var data = new List<Models.Simulation>();
|
||||
File.WriteAllText(this.tempStoragePath, JsonConvert.SerializeObject(data));
|
||||
this.log.Warn("Invalid simulation ID.", () => new { patch.Id });
|
||||
throw new InvalidInputException("Invalid simulation ID. Use ID '" + SimulationId + "'.");
|
||||
}
|
||||
|
||||
var item = await this.storage.GetAsync(StorageCollection, patch.Id);
|
||||
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
|
||||
simulation.Etag = item.ETag;
|
||||
|
||||
// Even when there's nothing to do, verify the etag mismatch
|
||||
if (patch.Etag != simulation.Etag)
|
||||
{
|
||||
this.log.Warn("Etag mismatch",
|
||||
() => new { Current = simulation.Etag, Provided = patch.Etag });
|
||||
throw new ConflictingResourceException(
|
||||
$"The ETag provided doesn't match the current resource ETag ({simulation.Etag}).");
|
||||
}
|
||||
|
||||
if (!patch.Enabled.HasValue || patch.Enabled.Value == simulation.Enabled)
|
||||
{
|
||||
// Nothing to do
|
||||
return simulation;
|
||||
}
|
||||
|
||||
simulation.Enabled = patch.Enabled.Value;
|
||||
simulation.Modified = DateTimeOffset.UtcNow;
|
||||
simulation.Version += 1;
|
||||
|
||||
item = await this.storage.UpdateAsync(
|
||||
StorageCollection,
|
||||
SimulationId,
|
||||
JsonConvert.SerializeObject(simulation),
|
||||
patch.Etag);
|
||||
|
||||
simulation.Etag = item.ETag;
|
||||
|
||||
return simulation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public interface IStorageAdapterClient
|
||||
{
|
||||
Task<ValueListApiModel> GetAllAsync(string collectionId);
|
||||
Task<ValueApiModel> GetAsync(string collectionId, string key);
|
||||
Task<ValueApiModel> CreateAsync(string collectionId, string value);
|
||||
Task<ValueApiModel> UpdateAsync(string collectionId, string key, string value, string etag);
|
||||
Task DeleteAsync(string collectionId, string key);
|
||||
}
|
||||
|
||||
// TODO: handle retriable errors
|
||||
public class StorageAdapterClient : IStorageAdapterClient
|
||||
{
|
||||
// TODO: make it configurable, default to false
|
||||
private const bool AllowInsecureSSLServer = true;
|
||||
|
||||
private readonly IHttpClient httpClient;
|
||||
private readonly ILogger logger;
|
||||
private readonly string serviceUri;
|
||||
private readonly int timeout;
|
||||
|
||||
public StorageAdapterClient(
|
||||
IHttpClient httpClient,
|
||||
IServicesConfig config,
|
||||
ILogger logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.logger = logger;
|
||||
this.serviceUri = config.StorageAdapterApiUrl;
|
||||
this.timeout = config.StorageAdapterApiTimeout;
|
||||
}
|
||||
|
||||
public async Task<ValueListApiModel> GetAllAsync(string collectionId)
|
||||
{
|
||||
var response = await httpClient.GetAsync(
|
||||
this.PrepareRequest($"collections/{collectionId}/values"));
|
||||
|
||||
ThrowIfError(response, collectionId, "");
|
||||
|
||||
return JsonConvert.DeserializeObject<ValueListApiModel>(response.Content);
|
||||
}
|
||||
|
||||
public async Task<ValueApiModel> GetAsync(string collectionId, string key)
|
||||
{
|
||||
var response = await httpClient.GetAsync(
|
||||
this.PrepareRequest($"collections/{collectionId}/values/{key}"));
|
||||
|
||||
ThrowIfError(response, collectionId, key);
|
||||
|
||||
return JsonConvert.DeserializeObject<ValueApiModel>(response.Content);
|
||||
}
|
||||
|
||||
public async Task<ValueApiModel> CreateAsync(string collectionId, string value)
|
||||
{
|
||||
var response = await httpClient.PostAsync(
|
||||
this.PrepareRequest($"collections/{collectionId}/values",
|
||||
new ValueApiModel { Data = value }));
|
||||
|
||||
ThrowIfError(response, collectionId, "");
|
||||
|
||||
return JsonConvert.DeserializeObject<ValueApiModel>(response.Content);
|
||||
}
|
||||
|
||||
public async Task<ValueApiModel> UpdateAsync(string collectionId, string key, string value, string etag)
|
||||
{
|
||||
var response = await httpClient.PutAsync(
|
||||
this.PrepareRequest($"collections/{collectionId}/values/{key}",
|
||||
new ValueApiModel { Data = value, ETag = etag }));
|
||||
|
||||
ThrowIfError(response, collectionId, key);
|
||||
|
||||
return JsonConvert.DeserializeObject<ValueApiModel>(response.Content);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string collectionId, string key)
|
||||
{
|
||||
var response = await httpClient.DeleteAsync(
|
||||
this.PrepareRequest($"collections/{collectionId}/values/{key}"));
|
||||
|
||||
ThrowIfError(response, collectionId, key);
|
||||
}
|
||||
|
||||
private HttpRequest PrepareRequest(string path, ValueApiModel content = null)
|
||||
{
|
||||
var request = new HttpRequest();
|
||||
request.AddHeader(HttpRequestHeader.Accept.ToString(), "application/json");
|
||||
request.AddHeader(HttpRequestHeader.CacheControl.ToString(), "no-cache");
|
||||
request.AddHeader(HttpRequestHeader.Referer.ToString(), "Device Simulation " + this.GetType().FullName);
|
||||
request.SetUriFromString($"{this.serviceUri}/{path}");
|
||||
request.Options.EnsureSuccess = false;
|
||||
request.Options.Timeout = this.timeout;
|
||||
if (this.serviceUri.ToLowerInvariant().StartsWith("https:"))
|
||||
{
|
||||
request.Options.AllowInsecureSSLServer = AllowInsecureSSLServer;
|
||||
}
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
request.SetContent(content);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private void ThrowIfError(IHttpResponse response, string collectionId, string key)
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new ResourceNotFoundException($"Resource {collectionId}/{key} not found.");
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
throw new ConflictingResourceException(
|
||||
$"Resource {collectionId}/{key} out of date. Reload the resource and retry.");
|
||||
}
|
||||
|
||||
if (response.IsError)
|
||||
{
|
||||
throw new ExternalDependencyException(
|
||||
new HttpRequestException($"Storage request error: status code {response.StatusCode}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
||||
{
|
||||
public class ValueApiModel
|
||||
{
|
||||
[JsonProperty("Key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonProperty("Data")]
|
||||
public string Data { get; set; }
|
||||
|
||||
[JsonProperty("ETag")]
|
||||
public string ETag { get; set; }
|
||||
|
||||
[JsonProperty("$metadata")]
|
||||
public Dictionary<string, string> Metadata;
|
||||
|
||||
public ValueApiModel()
|
||||
{
|
||||
this.Metadata = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter
|
||||
{
|
||||
public class ValueListApiModel
|
||||
{
|
||||
public List<ValueApiModel> Items { get; set; }
|
||||
|
||||
[JsonProperty("$metadata")]
|
||||
public Dictionary<string, string> Metadata { get; set; }
|
||||
|
||||
public ValueListApiModel()
|
||||
{
|
||||
this.Items = new List<ValueApiModel>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent
|
|||
// Print some useful information at bootstrap time
|
||||
PrintBootstrapInfo(container);
|
||||
|
||||
container.Resolve<ISimulation>().Run();
|
||||
container.Resolve<ISimulation>().RunAsync().Wait();
|
||||
}
|
||||
|
||||
private static void PrintBootstrapInfo(IContainer container)
|
||||
|
|
|
@ -23,6 +23,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Runtime
|
|||
private const string DeviceTypesScriptsFolderKey = ApplicationKey + "device_types_scripts_folder";
|
||||
private const string IoTHubConnStringKey = ApplicationKey + "iothub_connstring";
|
||||
|
||||
private const string StorageAdapterKey = "storageadapter:";
|
||||
private const string StorageAdapterApiUrlKey = StorageAdapterKey + "webservice_url";
|
||||
private const string StorageAdapterApiTimeoutKey = StorageAdapterKey + "webservice_timeout";
|
||||
|
||||
/// <summary>Service layer configuration</summary>
|
||||
public IServicesConfig ServicesConfig { get; }
|
||||
|
||||
|
@ -50,7 +54,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Runtime
|
|||
{
|
||||
DeviceTypesFolder = MapRelativePath(configData.GetString(DeviceTypesFolderKey)),
|
||||
DeviceTypesScriptsFolder = MapRelativePath(configData.GetString(DeviceTypesScriptsFolderKey)),
|
||||
IoTHubConnString = connstring
|
||||
IoTHubConnString = connstring,
|
||||
StorageAdapterApiUrl = configData.GetString(StorageAdapterApiUrlKey),
|
||||
StorageAdapterApiTimeout = configData.GetInt(StorageAdapterApiTimeoutKey)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -373,5 +373,11 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
|
|||
return JsonConvert.DeserializeObject<T>(
|
||||
JsonConvert.SerializeObject(source));
|
||||
}
|
||||
|
||||
private class TelemetryContext
|
||||
{
|
||||
public DeviceActor Self { get; set; }
|
||||
public DeviceType.DeviceTypeMessage Message { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
|
||||
|
@ -9,7 +11,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
|
|||
{
|
||||
public interface ISimulation
|
||||
{
|
||||
void Run();
|
||||
Task RunAsync();
|
||||
}
|
||||
|
||||
public class Simulation : ISimulation
|
||||
|
@ -30,14 +32,24 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
|
|||
this.runner = runner;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
public async Task RunAsync()
|
||||
{
|
||||
this.log.Info("Simulation Agent running", () => { });
|
||||
|
||||
// Keep running, checking if the simulation state changes
|
||||
while (true)
|
||||
{
|
||||
var simulation = this.simulations.GetList().FirstOrDefault();
|
||||
Services.Models.Simulation simulation = null;
|
||||
|
||||
try
|
||||
{
|
||||
simulation = (await this.simulations.GetListAsync()).FirstOrDefault();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unable to load simulation", () => new { e });
|
||||
}
|
||||
|
||||
if (simulation != null && simulation.Enabled)
|
||||
{
|
||||
this.runner.Start(simulation);
|
||||
|
|
|
@ -5,3 +5,7 @@ iothub_connstring = "${PCS_IOTHUB_CONNSTRING}"
|
|||
# which can be mounted to inject custom device types and scripts.
|
||||
device_types_folder = ./data/DeviceTypes/
|
||||
device_types_scripts_folder = ./data/DeviceTypes/Scripts/
|
||||
|
||||
[storageadapter]
|
||||
webservice_url = "${PCS_STORAGEADAPTER_WEBSERVICE_URL}"
|
||||
webservice_timeout = 10000
|
||||
|
|
|
@ -31,9 +31,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.Runtime
|
|||
private const string IoTHubConnStringKey = ApplicationKey + "iothub_connstring";
|
||||
private const string CorsWhitelistKey = ApplicationKey + "cors_whitelist";
|
||||
|
||||
private const string IoTHubManagerKey = "iothubmanager:";
|
||||
private const string IoTHubManagerApiUrlKey = IoTHubManagerKey + "webservice_url";
|
||||
private const string IoTHubManagerApiTimeoutKey = IoTHubManagerKey + "webservice_timeout";
|
||||
private const string StorageAdapterKey = "storageadapter:";
|
||||
private const string StorageAdapterApiUrlKey = StorageAdapterKey + "webservice_url";
|
||||
private const string StorageAdapterApiTimeoutKey = StorageAdapterKey + "webservice_timeout";
|
||||
|
||||
/// <summary>Web service listening port</summary>
|
||||
public int Port { get; }
|
||||
|
@ -71,7 +71,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.Runtime
|
|||
{
|
||||
DeviceTypesFolder = MapRelativePath(configData.GetString(DeviceTypesFolderKey)),
|
||||
DeviceTypesScriptsFolder = MapRelativePath(configData.GetString(DeviceTypesScriptsFolderKey)),
|
||||
IoTHubConnString = connstring
|
||||
IoTHubConnString = connstring,
|
||||
StorageAdapterApiUrl = configData.GetString(StorageAdapterApiUrlKey),
|
||||
StorageAdapterApiTimeout = configData.GetInt(StorageAdapterApiTimeoutKey)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,3 +7,7 @@ cors_whitelist = "{ 'origins': ['*'], 'methods': ['*'], 'headers': ['*'] }"
|
|||
# which can be mounted to inject custom device types and scripts.
|
||||
device_types_folder = ./data/DeviceTypes/
|
||||
device_types_scripts_folder = ./data/DeviceTypes/Scripts/
|
||||
|
||||
[storageadapter]
|
||||
webservice_url = "${PCS_STORAGEADAPTER_WEBSERVICE_URL}"
|
||||
webservice_timeout = 10000
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
|
@ -24,19 +25,19 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
|
|||
}
|
||||
|
||||
[HttpGet]
|
||||
public SimulationListApiModel Get()
|
||||
public async Task<SimulationListApiModel> GetAsync()
|
||||
{
|
||||
return new SimulationListApiModel(this.simulationsService.GetList());
|
||||
return new SimulationListApiModel(await this.simulationsService.GetListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public SimulationApiModel Get(string id)
|
||||
public async Task<SimulationApiModel> GetAsync(string id)
|
||||
{
|
||||
return new SimulationApiModel(this.simulationsService.Get(id));
|
||||
return new SimulationApiModel(await this.simulationsService.GetAsync(id));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public SimulationApiModel Post(
|
||||
public async Task<SimulationApiModel> PostAsync(
|
||||
[FromBody] SimulationApiModel simulation,
|
||||
[FromQuery(Name = "template")] string template = "")
|
||||
{
|
||||
|
@ -44,8 +45,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
|
|||
{
|
||||
if (string.IsNullOrEmpty(template))
|
||||
{
|
||||
this.log.Warn("No data or invalid data provided",
|
||||
() => new { simulation, template });
|
||||
this.log.Warn("No data or invalid data provided", () => new { simulation, template });
|
||||
throw new BadRequestException("No data or invalid data provided.");
|
||||
}
|
||||
|
||||
|
@ -53,33 +53,26 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
|
|||
}
|
||||
|
||||
return new SimulationApiModel(
|
||||
this.simulationsService.Insert(simulation.ToServiceModel(), template));
|
||||
await this.simulationsService.InsertAsync(simulation.ToServiceModel(), template));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public SimulationApiModel Put(
|
||||
public async Task<SimulationApiModel> PutAsync(
|
||||
[FromBody] SimulationApiModel simulation,
|
||||
string id = "",
|
||||
[FromQuery(Name = "template")] string template = "")
|
||||
string id = "")
|
||||
{
|
||||
if (simulation == null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template))
|
||||
{
|
||||
this.log.Warn("No data or invalid data provided",
|
||||
() => new { id, simulation, template });
|
||||
throw new BadRequestException("No data or invalid data provided.");
|
||||
}
|
||||
|
||||
simulation = new SimulationApiModel();
|
||||
this.log.Warn("No data or invalid data provided", () => new { simulation });
|
||||
throw new BadRequestException("No data or invalid data provided.");
|
||||
}
|
||||
|
||||
return new SimulationApiModel(
|
||||
this.simulationsService.Upsert(simulation.ToServiceModel(id), template));
|
||||
await this.simulationsService.UpsertAsync(simulation.ToServiceModel(id)));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
public SimulationApiModel Patch(
|
||||
public async Task<SimulationApiModel> PatchAsync(
|
||||
string id,
|
||||
[FromBody] SimulationPatchApiModel patch)
|
||||
{
|
||||
|
@ -90,7 +83,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
|
|||
}
|
||||
|
||||
return new SimulationApiModel(
|
||||
this.simulationsService.Merge(patch.ToServiceModel(id)));
|
||||
await this.simulationsService.MergeAsync(patch.ToServiceModel(id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
|
|||
statusMsg = "Unable to use Azure IoT Hub service";
|
||||
}
|
||||
|
||||
var simulation = this.simulations.GetList().FirstOrDefault();
|
||||
var simulation = (await this.simulations.GetListAsync()).FirstOrDefault();
|
||||
var running = (simulation != null && simulation.Enabled);
|
||||
|
||||
var result = new StatusApiModel(statusIsOk, statusMsg);
|
||||
|
|
|
@ -4,3 +4,8 @@ if [[ -z "$PCS_IOTHUB_CONNSTRING" ]]; then
|
|||
echo "Error: the PCS_IOTHUB_CONNSTRING environment variable is not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$PCS_STORAGEADAPTER_WEBSERVICE_URL" ]]; then
|
||||
echo "Error: the PCS_STORAGEADAPTER_WEBSERVICE_URL environment variable is not defined."
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
@ -5,4 +5,9 @@ IF "%PCS_IOTHUB_CONNSTRING%" == "" (
|
|||
exit /B 1
|
||||
)
|
||||
|
||||
IF "%PCS_STORAGEADAPTER_WEBSERVICE_URL%" == "" (
|
||||
echo Error: the PCS_STORAGEADAPTER_WEBSERVICE_URL environment variable is not defined.
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
endlocal
|
||||
|
|
|
@ -10,3 +10,6 @@
|
|||
|
||||
# Azure IoT Hub Connection string
|
||||
export PCS_IOTHUB_CONNSTRING="..."
|
||||
|
||||
# Endpoint to reach the storage adapter
|
||||
export PCS_STORAGEADAPTER_WEBSERVICE_URL="http://127.0.0.1:9022/v1"
|
||||
|
|
|
@ -8,3 +8,6 @@
|
|||
|
||||
:: Azure IoT Hub Connection string
|
||||
SETX PCS_IOTHUB_CONNSTRING "..."
|
||||
|
||||
:: Endpoint to reach the storage adapter
|
||||
SETX PCS_STORAGEADAPTER_WEBSERVICE_URL "http://127.0.0.1:9022/v1"
|
||||
|
|
17
scripts/run
17
scripts/run
|
@ -6,6 +6,7 @@
|
|||
# Run the service inside a Docker container: ./scripts/run --in-sandbox
|
||||
# Run only the web service: ./scripts/run --webservice
|
||||
# Run only the simulation: ./scripts/run --simulation
|
||||
# Run only the simulation: ./scripts/run --storageadapter
|
||||
# Show how to use this script: ./scripts/run -h
|
||||
# Show how to use this script: ./scripts/run --help
|
||||
|
||||
|
@ -24,10 +25,11 @@ PCS_CACHE="/tmp/azure/iotpcs/.cache"
|
|||
help() {
|
||||
echo "Usage:"
|
||||
echo " Run the service in the local environment: ./scripts/run"
|
||||
echo " Run the service inside a Docker container: ./scripts/run -s|--in-sandbox"
|
||||
echo " Run the service inside a Docker container: ./scripts/run -s | --in-sandbox"
|
||||
echo " Run only the web service: ./scripts/run --webservice"
|
||||
echo " Run only the simulation: ./scripts/run --simulation"
|
||||
echo " Show how to use this script: ./scripts/run -h|--help"
|
||||
echo " Run the storage adapter dependency: ./scripts/run --storageadapter"
|
||||
echo " Show how to use this script: ./scripts/run -h | --help"
|
||||
}
|
||||
|
||||
setup_sandbox_cache() {
|
||||
|
@ -91,10 +93,17 @@ run_webservice() {
|
|||
}
|
||||
|
||||
run_simulation() {
|
||||
echo "Starting simulation..."
|
||||
echo "Starting simulation agent..."
|
||||
dotnet run --configuration $CONFIGURATION --project SimulationAgent/*.csproj
|
||||
}
|
||||
|
||||
run_storageadapter() {
|
||||
echo "Starting storage adapter..."
|
||||
docker run -it -p 9022:9022 \
|
||||
-e "PCS_STORAGEADAPTER_DOCUMENTDB_CONNSTRING=$PCS_STORAGEADAPTER_DOCUMENTDB_CONNSTRING" \
|
||||
azureiotpcs/pcs-storage-adapter-dotnet
|
||||
}
|
||||
|
||||
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
|
||||
help
|
||||
elif [[ "$1" == "--in-sandbox" || "$1" == "-s" ]]; then
|
||||
|
@ -108,6 +117,8 @@ elif [[ "$1" == "--webservice" ]]; then
|
|||
elif [[ "$1" == "--simulation" ]]; then
|
||||
prepare_for_run
|
||||
run_simulation
|
||||
elif [[ "$1" == "--storageadapter" ]]; then
|
||||
run_storageadapter
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:: Run the service inside a Docker container: scripts\run --in-sandbox
|
||||
:: Run only the web service: scripts\run --webservice
|
||||
:: Run only the simulation: scripts\run --simulation
|
||||
:: Run only the simulation: scripts\run --storageadapter
|
||||
:: Show how to use this script: scripts\run -h
|
||||
:: Show how to use this script: scripts\run --help
|
||||
|
||||
|
@ -23,15 +24,17 @@ IF "%1"=="-s" GOTO :RunInSandbox
|
|||
IF "%1"=="--in-sandbox" GOTO :RunInSandbox
|
||||
IF "%1"=="--webservice" GOTO :RunWebService
|
||||
IF "%1"=="--simulation" GOTO :RunSimulation
|
||||
IF "%1"=="--storageadapter" GOTO :RunStorageAdapter
|
||||
|
||||
:Help
|
||||
|
||||
echo "Usage:"
|
||||
echo " Run the service in the local environment: ./scripts/run"
|
||||
echo " Run the service inside a Docker container: ./scripts/run -s|--in-sandbox"
|
||||
echo " Run the service inside a Docker container: ./scripts/run -s | --in-sandbox"
|
||||
echo " Run only the web service: ./scripts/run --webservice"
|
||||
echo " Run only the simulation: ./scripts/run --simulation"
|
||||
echo " Show how to use this script: ./scripts/run -h|--help"
|
||||
echo " Run the storage adapter dependency: ./scripts/run --storageadapter"
|
||||
echo " Show how to use this script: ./scripts/run -h | --help"
|
||||
|
||||
goto :END
|
||||
|
||||
|
@ -50,6 +53,7 @@ IF "%1"=="--simulation" GOTO :RunSimulation
|
|||
call dotnet restore
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
echo Starting simulation agent and web service...
|
||||
start "" dotnet run --configuration %CONFIGURATION% --project SimulationAgent/SimulationAgent.csproj
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
|
@ -73,6 +77,7 @@ IF "%1"=="--simulation" GOTO :RunSimulation
|
|||
call dotnet restore
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
echo Starting web service...
|
||||
dotnet run --configuration %CONFIGURATION% --project WebService/WebService.csproj
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
|
@ -93,6 +98,7 @@ IF "%1"=="--simulation" GOTO :RunSimulation
|
|||
call dotnet restore
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
echo Starting simulation agent...
|
||||
dotnet run --configuration %CONFIGURATION% --project SimulationAgent/SimulationAgent.csproj
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
|
@ -136,6 +142,20 @@ IF "%1"=="--simulation" GOTO :RunSimulation
|
|||
|
||||
goto :END
|
||||
|
||||
:RunStorageAdapter
|
||||
|
||||
echo Starting storage adapter...
|
||||
|
||||
docker run -it -p 9022:9022 ^
|
||||
-e "PCS_STORAGEADAPTER_DOCUMENTDB_CONNSTRING=%PCS_STORAGEADAPTER_DOCUMENTDB_CONNSTRING%" ^
|
||||
azureiotpcs/pcs-storage-adapter-dotnet
|
||||
|
||||
:: Error 125 typically triggers in Windows if the drive is not shared
|
||||
IF %ERRORLEVEL% EQU 125 GOTO DOCKER_SHARE
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO FAIL
|
||||
|
||||
goto :END
|
||||
|
||||
|
||||
|
||||
:: - - - - - - - - - - - - - -
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
0.1.4
|
||||
0.1.5
|
||||
|
|
Загрузка…
Ссылка в новой задаче