Merge pull request #160 from Azure/feature/edge

Merge Edge Feature Branch into Master
This commit is contained in:
Parvez 2018-10-16 14:28:50 -07:00 коммит произвёл GitHub
Родитель e6e72907fb 5cfa711b7f
Коммит ac04645ed1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
41 изменённых файлов: 2367 добавлений и 34 удалений

Просмотреть файл

@ -134,7 +134,11 @@ namespace Services.Test
"UpdateRules",
"DeleteRules",
"CreateJobs",
"UpdateSimManagement"
"UpdateSimManagement",
"CreateDeployments",
"DeleteDeployments",
"CreatePackages",
"DeletePackages"
};
return new Policy()
@ -157,7 +161,11 @@ namespace Services.Test
"CreateRules",
"UpdateRules",
"CreateJobs",
"UpdateSimManagement"
"UpdateSimManagement",
"CreateDeployments",
"DeleteDeployments",
"CreatePackages",
"DeletePackages"
};
return new Policy()

Просмотреть файл

@ -16,7 +16,11 @@
"UpdateRules",
"DeleteRules",
"CreateJobs",
"UpdateSimManagement"
"UpdateSimManagement",
"CreateDeployments",
"DeleteDeployments",
"CreatePackages",
"DeletePackages"
]
},
{

Просмотреть файл

@ -26,7 +26,11 @@ Role Policy Example:
"UpdateRules",
"DeleteRules",
"CreateJobs",
"UpdateSimManagement"
"UpdateSimManagement",
"CreateDeployment",
"DeleteDeployment",
"CreatePackage",
"DeletePackage"
]
},
{

Просмотреть файл

@ -24,6 +24,8 @@ More information [here][rm-arch-url].
* Get or upload logo
* Get or set overall solution settings
* Get or set individual user settings
* Create or delete a package
* Get all or a single uploaded package.
## Documentation
* View the API documentation in the [Wiki](https://github.com/Azure/pcs-config-dotnet/wiki)

Просмотреть файл

@ -11,6 +11,7 @@ using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Runtime;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Services.Test.helpers;
using Xunit;
@ -22,6 +23,70 @@ namespace Services.Test
private readonly Mock<IStorageAdapterClient> mockClient;
private readonly Storage storage;
private readonly Random rand;
private const string PACKAGES_COLLECTION_ID = "packages";
private const string TEST_PACKAGE_JSON =
@"{
""id"": ""tempid"",
""schemaVersion"": ""1.0"",
""content"": {
""modulesContent"": {
""$edgeAgent"": {
""properties.desired"": {
""schemaVersion"": ""1.0"",
""runtime"": {
""type"": ""docker"",
""settings"": {
""loggingOptions"": """",
""minDockerVersion"": ""v1.25""
}
},
""systemModules"": {
""edgeAgent"": {
""type"": ""docker"",
""settings"": {
""image"": ""mcr.microsoft.com/azureiotedge-agent:1.0"",
""createOptions"": ""{}""
}
},
""edgeHub"": {
""type"": ""docker"",
""settings"": {
""image"": ""mcr.microsoft.com/azureiotedge-hub:1.0"",
""createOptions"": ""{}""
},
""status"": ""running"",
""restartPolicy"": ""always""
}
},
""modules"": {}
}
},
""$edgeHub"": {
""properties.desired"": {
""schemaVersion"": ""1.0"",
""routes"": {
""route"": ""FROM /messages/* INTO $upstream""
},
""storeAndForwardConfiguration"": {
""timeToLiveSecs"": 7200
}
}
}
}
},
""targetCondition"": ""*"",
""priority"": 30,
""labels"": {
""Name"": ""Test""
},
""createdTimeUtc"": ""2018-08-20T18:05:55.482Z"",
""lastUpdatedTimeUtc"": ""2018-08-20T18:05:55.482Z"",
""etag"": null,
""metrics"": {
""results"": {},
""queries"": {}
}
}";
public StorageTest()
{
@ -599,5 +664,98 @@ namespace Services.Test
return result;
}
[Fact]
public async Task AddPackageTest()
{
// Arrange
const string collectionId = "packages";
const string key = "package name";
var pkg = new Package
{
Id = string.Empty,
Name = key,
Type = PackageType.EdgeManifest,
Content = TEST_PACKAGE_JSON
};
var value = JsonConvert.SerializeObject(pkg);
this.mockClient
.Setup(x => x.CreateAsync(
It.Is<string>(i => i == collectionId),
It.Is<string>(i => this.IsMatchingPackage(i, value))))
.ReturnsAsync(new ValueApiModel
{
Key = key,
Data = value
});
// Act
var result = await this.storage.AddPackageAsync(pkg);
// Assert
Assert.Equal(pkg.Name, result.Name);
Assert.Equal(pkg.Type, result.Type);
Assert.Equal(pkg.Content, result.Content);
}
[Fact]
public async Task InvalidPackageThrowsTest()
{
// Arrange
var pkg = new Package
{
Id = string.Empty,
Name = "testpackage",
Type = PackageType.EdgeManifest,
Content = "InvalidPackage"
};
// Act & Assert
await Assert.ThrowsAsync<InvalidInputException>(async () =>
await this.storage.AddPackageAsync(pkg));
}
[Fact]
public async Task DeletePackageAsyncTest()
{
// Arrange
var packageId = this.rand.NextString();
this.mockClient
.Setup(x => x.DeleteAsync(It.Is<string>(s => s == PACKAGES_COLLECTION_ID),
It.Is<string>(s => s == packageId)))
.Returns(Task.FromResult(0));
// Act
await this.storage.DeletePackageAsync(packageId);
// Assert
this.mockClient
.Verify(x => x.DeleteAsync(
It.Is<string>(s => s == PACKAGES_COLLECTION_ID),
It.Is<string>(s => s == packageId)),
Times.Once);
}
private bool IsMatchingPackage(string pkgJson, string originalPkgJson)
{
const string dateCreatedField = "DateCreated";
var createdPkg = JObject.Parse(pkgJson);
var originalPkg = JObject.Parse(originalPkgJson);
// To prevent false failures on unit tests we allow a couple of seconds diffence
// when verifying the date created.
var dateCreated = DateTimeOffset.Parse(createdPkg[dateCreatedField].ToString());
var secondsDiff = (DateTimeOffset.UtcNow - dateCreated).TotalSeconds;
if (secondsDiff > 3)
{
return false;
}
createdPkg.Remove(dateCreatedField);
originalPkg.Remove(dateCreatedField);
return JToken.DeepEquals(createdPkg, originalPkg);
}
}
}

Просмотреть файл

@ -1,5 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.IoTSolutions.UIConfig.Services.Models
{
public class Package
{
public string Id { get; set; }
public string Name { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PackageType Type { get; set; }
public string Content { get; set; }
public string DateCreated { get; set; }
}
public enum PackageType
{
EdgeManifest
}
}

Просмотреть файл

@ -18,6 +18,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />

Просмотреть файл

@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.External;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
@ -25,6 +27,10 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services
Task<DeviceGroup> CreateDeviceGroupAsync(DeviceGroup input);
Task<DeviceGroup> UpdateDeviceGroupAsync(string id, DeviceGroup input, string etag);
Task DeleteDeviceGroupAsync(string id);
Task<IEnumerable<Package>> GetPackagesAsync();
Task<Package> GetPackageAsync(string id);
Task<Package> AddPackageAsync(Package package);
Task DeletePackageAsync(string id);
}
public class Storage : IStorage
@ -37,7 +43,9 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services
internal const string LOGO_KEY = "logo";
internal const string USER_COLLECTION_ID = "user-settings";
internal const string DEVICE_GROUP_COLLECTION_ID = "devicegroups";
internal const string PACKAGES_COLLECTION_ID = "packages";
private const string AZURE_MAPS_KEY = "AzureMapsKey";
private const string DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:sszzz";
public Storage(
IStorageAdapterClient client,
@ -169,6 +177,45 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services
await this.client.DeleteAsync(DEVICE_GROUP_COLLECTION_ID, id);
}
public async Task<IEnumerable<Package>> GetPackagesAsync()
{
var response = await this.client.GetAllAsync(PACKAGES_COLLECTION_ID);
return response.Items.AsParallel().Select(this.CreatePackageServiceModel);
}
public async Task<Package> AddPackageAsync(Package package)
{
try
{
JsonConvert.DeserializeObject<Configuration>(package.Content);
}
catch (Exception)
{
throw new InvalidInputException("Package provided is not a valid deployment manifest");
}
package.DateCreated = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
var value = JsonConvert.SerializeObject(package,
Formatting.Indented,
new JsonSerializerSettings {
NullValueHandling = NullValueHandling.Ignore
});
var response = await this.client.CreateAsync(PACKAGES_COLLECTION_ID, value);
return this.CreatePackageServiceModel(response);
}
public async Task DeletePackageAsync(string id)
{
await this.client.DeleteAsync(PACKAGES_COLLECTION_ID, id);
}
public async Task<Package> GetPackageAsync(string id)
{
var response = await this.client.GetAsync(PACKAGES_COLLECTION_ID, id);
return this.CreatePackageServiceModel(response);
}
private DeviceGroup CreateGroupServiceModel(ValueApiModel input)
{
var output = JsonConvert.DeserializeObject<DeviceGroup>(input.Data);
@ -176,5 +223,12 @@ namespace Microsoft.Azure.IoTSolutions.UIConfig.Services
output.ETag = input.ETag;
return output;
}
private Package CreatePackageServiceModel(ValueApiModel input)
{
var output = JsonConvert.DeserializeObject<Package>(input.Data);
output.Id = input.Key;
return output;
}
}
}

Просмотреть файл

@ -0,0 +1,156 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Azure.IoTSolutions.UIConfig.Services;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers;
using Moq;
using WebService.Test.helpers;
using Xunit;
namespace WebService.Test.Controllers
{
public class PackageControllerTest
{
private readonly Mock<IStorage> mockStorage;
private readonly PackageController controller;
private readonly Random rand;
private const string DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:sszzz";
public PackageControllerTest()
{
this.mockStorage = new Mock<IStorage>();
this.controller = new PackageController(this.mockStorage.Object);
this.rand = new Random();
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("EdgeManifest", "filename", true, false)]
[InlineData("EdgeManifest", "filename", false, true)]
[InlineData("EdgeManifest", "", true, true)]
[InlineData("BAD_TYPE", "filename", true, true)]
public async Task PostAsyncExceptionVerificationTest(string type, string filename,
bool isValidFileProvided, bool expectException)
{
// Arrange
IFormFile file = null;
if (isValidFileProvided)
{
file = this.CreateSampleFile(filename);
}
this.mockStorage.Setup(x => x.AddPackageAsync(
It.Is<Package>(p => p.Type.ToString().Equals(type) &&
p.Name.Equals(filename))))
.ReturnsAsync(new Package() {
Name = filename,
Type = PackageType.EdgeManifest
});
try
{
// Act
var package = await this.controller.PostAsync(type, file);
// Assert
Assert.False(expectException);
Assert.Equal(filename, package.Name);
Assert.Equal(type, package.Type.ToString());
}
catch (Exception)
{
Assert.True(expectException);
}
}
[Fact]
public async Task GetPackageTest()
{
// Arrange
const string id = "packageId";
const string name = "packageName";
const PackageType type = PackageType.EdgeManifest;
const string content = "{}";
string dateCreated = DateTime.UtcNow.ToString(DATE_FORMAT);
this.mockStorage
.Setup(x => x.GetPackageAsync(id))
.ReturnsAsync(new Package()
{
Id = id,
Name = name,
Content = content,
Type = type,
DateCreated = dateCreated
});
// Act
var pkg = await this.controller.GetAsync(id);
// Assert
this.mockStorage
.Verify(x => x.GetPackageAsync(id), Times.Once);
Assert.Equal(id, pkg.Id);
Assert.Equal(name, pkg.Name);
Assert.Equal(type, pkg.Type);
Assert.Equal(content, pkg.Content);
Assert.Equal(dateCreated, pkg.DateCreated);
}
[Fact]
public async Task GetAllPackageTest()
{
// Arrange
const string id = "packageId";
const string name = "packageName";
const PackageType type = PackageType.EdgeManifest;
const string content = "{}";
string dateCreated = DateTime.UtcNow.ToString(DATE_FORMAT);
int[] idx = new int[] {0, 1, 2};
var packages = idx.Select(i => new Package()
{
Id = id + i,
Name = name + i,
Content = content + i,
Type = type + i,
DateCreated = dateCreated
}).ToList();
this.mockStorage
.Setup(x => x.GetPackagesAsync())
.ReturnsAsync(packages);
// Act
var resultPackages = await this.controller.GetAllAsync();
// Assert
this.mockStorage
.Verify(x => x.GetPackagesAsync(), Times.Once);
foreach (int i in idx)
{
var pkg = resultPackages.Items.ElementAt(i);
Assert.Equal(id + i, pkg.Id);
Assert.Equal(name + i, pkg.Name);
Assert.Equal(type + i, pkg.Type);
Assert.Equal(content + i, pkg.Content);
Assert.Equal(dateCreated, pkg.DateCreated);
}
}
private FormFile CreateSampleFile(string filename)
{
var stream = new MemoryStream();
stream.WriteByte(100);
stream.Flush();
stream.Position = 0;
return new FormFile(stream, 0, 1, "file", filename);
}
}
}

Просмотреть файл

@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.UIConfig.Services;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Filters;
using Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Models;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Controllers
{
[Route(Version.PATH + "/packages"), TypeFilter(typeof(ExceptionsFilterAttribute))]
public class PackageController : Controller
{
private readonly IStorage storage;
public PackageController(IStorage storage)
{
this.storage = storage;
}
[HttpGet]
public async Task<PackageListApiModel> GetAllAsync()
{
return new PackageListApiModel(await this.storage.GetPackagesAsync());
}
[HttpGet("{id}")]
public async Task<PackageApiModel> GetAsync(string id)
{
if (string.IsNullOrEmpty(id))
{
throw new InvalidInputException("Valid id must be provided");
}
return new PackageApiModel(await this.storage.GetPackageAsync(id));
}
[HttpPost]
[Authorize("CreatePackages")]
public async Task<PackageApiModel> PostAsync(string type, IFormFile package)
{
if (string.IsNullOrEmpty(type))
{
throw new InvalidInputException("Package type must be provided");
}
bool isValidPackageType = Enum.TryParse(type, true, out PackageType uploadedPackageType);
if (!isValidPackageType)
{
throw new InvalidInputException($"Provided package type {type} is not valid.");
}
if (package == null || package.Length == 0 || string.IsNullOrEmpty(package.FileName))
{
throw new InvalidInputException("Package uploaded is missing or invalid.");
}
string packageContent;
using (var streamReader = new StreamReader(package.OpenReadStream()))
{
packageContent = await streamReader.ReadToEndAsync();
}
var packageToAdd = new Package()
{
Content = packageContent,
Name = package.FileName,
Type = uploadedPackageType
};
return new PackageApiModel(await this.storage.AddPackageAsync(packageToAdd));
}
[HttpDelete("{id}")]
[Authorize("DeletePackages")]
public async Task DeleteAsync(string id)
{
if (string.IsNullOrEmpty(id))
{
throw new InvalidInputException("Valid id must be provided");
}
await this.storage.DeletePackageAsync(id);
}
}
}

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Models
{
public class PackageApiModel
{
[JsonProperty("Id")]
public string Id;
[JsonProperty("Name")]
public string Name { get; set; }
[JsonProperty("Type")]
[JsonConverter(typeof(StringEnumConverter))]
public PackageType Type { get; set; }
[JsonProperty(PropertyName = "DateCreated")]
public string DateCreated { get; set; }
[JsonProperty("Content")]
public string Content { get; set; }
public PackageApiModel(Package model)
{
this.Id = model.Id;
this.Name = model.Name;
this.Type = model.Type;
this.DateCreated = model.DateCreated;
this.Content = model.Content;
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.IoTSolutions.UIConfig.Services.Models;
namespace Microsoft.Azure.IoTSolutions.UIConfig.WebService.v1.Models
{
public class PackageListApiModel
{
public IEnumerable<PackageApiModel> Items { get; set; }
public PackageListApiModel(IEnumerable<Package> models)
{
this.Items = models.Select(m => new PackageApiModel(m));
}
}
}

Просмотреть файл

@ -0,0 +1,319 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Moq;
using Xunit;
using System.Threading.Tasks;
using Services.Test.helpers;
using Microsoft.Azure.Devices;
namespace Services.Test
{
public class DeploymentsTest
{
private readonly Deployments deployments;
private readonly Mock<RegistryManager> registry;
private const string DEPLOYMENT_NAME_LABEL = "Name";
private const string DEPLOYMENT_GROUP_ID_LABEL = "DeviceGroupId";
private const string DEPLOYMENT_GROUP_NAME_LABEL = "DeviceGroupName";
private const string DEPLOYMENT_PACKAGE_NAME_LABEL = "PackageName";
private const string RM_CREATED_LABEL = "RMDeployment";
private const string RESOURCE_NOT_FOUND_EXCEPTION =
"Microsoft.Azure.IoTSolutions.IotHubManager.Services." +
"Exceptions.ResourceNotSupportedException, Microsoft.Azure." +
"IoTSolutions.IotHubManager.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
private const string TEST_PACKAGE_JSON =
@"{
""id"": ""tempid"",
""schemaVersion"": ""1.0"",
""content"": {
""modulesContent"": {
""$edgeAgent"": {
""properties.desired"": {
""schemaVersion"": ""1.0"",
""runtime"": {
""type"": ""docker"",
""settings"": {
""loggingOptions"": """",
""minDockerVersion"": ""v1.25""
}
},
""systemModules"": {
""edgeAgent"": {
""type"": ""docker"",
""settings"": {
""image"": ""mcr.microsoft.com/azureiotedge-agent:1.0"",
""createOptions"": ""{}""
}
},
""edgeHub"": {
""type"": ""docker"",
""settings"": {
""image"": ""mcr.microsoft.com/azureiotedge-hub:1.0"",
""createOptions"": ""{}""
},
""status"": ""running"",
""restartPolicy"": ""always""
}
},
""modules"": {}
}
},
""$edgeHub"": {
""properties.desired"": {
""schemaVersion"": ""1.0"",
""routes"": {
""route"": ""FROM /messages/* INTO $upstream""
},
""storeAndForwardConfiguration"": {
""timeToLiveSecs"": 7200
}
}
}
}
},
""targetCondition"": ""*"",
""priority"": 30,
""labels"": {
""Name"": ""Test""
},
""createdTimeUtc"": ""2018-08-20T18:05:55.482Z"",
""lastUpdatedTimeUtc"": ""2018-08-20T18:05:55.482Z"",
""etag"": null,
""metrics"": {
""results"": {},
""queries"": {}
}
}";
public DeploymentsTest()
{
this.registry = new Mock<RegistryManager>();
this.deployments = new Deployments(this.registry.Object,
"mockIoTHub");
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("depname", "dvcgroupid", "dvcquery", TEST_PACKAGE_JSON, 10, "")]
[InlineData("", "dvcgroupid", "dvcquery", TEST_PACKAGE_JSON, 10, "System.ArgumentNullException")]
[InlineData("depname", "", "dvcquery", TEST_PACKAGE_JSON, 10, "System.ArgumentNullException")]
[InlineData("depname", "dvcgroupid", "", TEST_PACKAGE_JSON, 10, "System.ArgumentNullException")]
[InlineData("depname", "dvcgroupid", "dvcquery", "", 10, "System.ArgumentNullException")]
[InlineData("depname", "dvcgroupid", "dvcquery", TEST_PACKAGE_JSON, -1, "System.ArgumentOutOfRangeException")]
public async Task CreateDeploymentTest(string deploymentName, string deviceGroupId,
string deviceGroupQuery, string packageContent,
int priority, string expectedException)
{
// Arrange
var depModel = new DeploymentServiceModel()
{
Name = deploymentName,
DeviceGroupId = deviceGroupId,
DeviceGroupQuery = deviceGroupQuery,
PackageContent = packageContent,
Priority = priority
};
var newConfig = new Configuration("test-config")
{
Labels = new Dictionary<string, string>()
{
{ DEPLOYMENT_NAME_LABEL, deploymentName },
{ DEPLOYMENT_GROUP_ID_LABEL, deviceGroupId },
{ RM_CREATED_LABEL, bool.TrueString },
}, Priority = priority
};
this.registry.Setup(r => r.AddConfigurationAsync(It.Is<Configuration>(c =>
c.Labels.ContainsKey(DEPLOYMENT_NAME_LABEL) &&
c.Labels.ContainsKey(DEPLOYMENT_GROUP_ID_LABEL) &&
c.Labels.ContainsKey(RM_CREATED_LABEL) &&
c.Labels[DEPLOYMENT_NAME_LABEL] == deploymentName &&
c.Labels[DEPLOYMENT_GROUP_ID_LABEL] == deviceGroupId &&
c.Labels[RM_CREATED_LABEL] == bool.TrueString)))
.ReturnsAsync(newConfig);
// Act
if (string.IsNullOrEmpty(expectedException))
{
var createdDeployment = await this.deployments.CreateAsync(depModel);
// Assert
Assert.False(string.IsNullOrEmpty(createdDeployment.Id));
Assert.Equal(deploymentName, createdDeployment.Name);
Assert.Equal(deviceGroupId, createdDeployment.DeviceGroupId);
Assert.Equal(priority, createdDeployment.Priority);
}
else
{
await Assert.ThrowsAsync(Type.GetType(expectedException),
async () => await this.deployments.CreateAsync(depModel));
}
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task InvalidRmConfigurationTest()
{
// Arrange
var configuration = this.CreateConfiguration(0, false);
this.registry.Setup(r => r.GetConfigurationAsync(It.IsAny<string>()))
.ReturnsAsync(configuration);
// Act & Assert
await Assert.ThrowsAsync(Type.GetType(RESOURCE_NOT_FOUND_EXCEPTION),
async () => await this.deployments.GetAsync(configuration.Id));
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData(0)]
[InlineData(1)]
[InlineData(5)]
public async Task GetDeploymentsTest(int numDeployments)
{
// Arrange
var configurations = new List<Configuration>();
for (int i = numDeployments - 1; i >= 0; i--)
{
configurations.Add(this.CreateConfiguration(i, true));
}
this.registry.Setup(r => r.GetConfigurationsAsync(20)).ReturnsAsync(configurations);
// Act
var returnedDeployments = await this.deployments.ListAsync();
// Assert
Assert.Equal(numDeployments, returnedDeployments.Items.Count);
// verify deployments are ordered by name
for (int i = 0; i < numDeployments; i++)
{
Assert.Equal("deployment" + i, returnedDeployments.Items[i].Name);
}
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task GetDeploymentsWithDeviceStatusTest()
{
// Arrange
var configuration = this.CreateConfiguration(0, true);
var deploymentId = configuration.Id;
this.registry.Setup(r => r.GetConfigurationAsync(deploymentId)).ReturnsAsync(configuration);
IQuery queryResult = new ResultQuery(3);
this.registry.Setup(r => r.CreateQuery(It.IsAny<string>())).Returns(queryResult);
// Act
var returnedDeployment = await this.deployments.GetAsync(deploymentId);
var deviceStatuses = returnedDeployment.DeploymentMetrics.DeviceStatuses;
// Assert
Assert.Null(deviceStatuses);
returnedDeployment = await this.deployments.GetAsync(deploymentId, true);
deviceStatuses = returnedDeployment.DeploymentMetrics.DeviceStatuses;
Assert.Equal(3, deviceStatuses.Count);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task VerifyGroupAndPackageNameLabelsTest()
{
// Arrange
var deviceGroupId = "dvcGroupId";
var deviceGroupName = "dvcGroupName";
var deviceGroupQuery = "dvcGroupQuery";
var packageName = "packageName";
var deploymentName = "depName";
var priority = 10;
var depModel = new DeploymentServiceModel()
{
Name = deploymentName,
DeviceGroupId = deviceGroupId,
DeviceGroupName = deviceGroupName,
DeviceGroupQuery = deviceGroupQuery,
PackageContent = TEST_PACKAGE_JSON,
PackageName = packageName,
Priority = priority
};
var newConfig = new Configuration("test-config")
{
Labels = new Dictionary<string, string>()
{
{ DEPLOYMENT_NAME_LABEL, deploymentName },
{ DEPLOYMENT_GROUP_ID_LABEL, deviceGroupId },
{ RM_CREATED_LABEL, bool.TrueString },
{ DEPLOYMENT_GROUP_NAME_LABEL, deviceGroupName },
{ DEPLOYMENT_PACKAGE_NAME_LABEL, packageName }
},
Priority = priority
};
this.registry.Setup(r => r.AddConfigurationAsync(It.Is<Configuration>(c =>
c.Labels.ContainsKey(DEPLOYMENT_NAME_LABEL) &&
c.Labels.ContainsKey(DEPLOYMENT_GROUP_ID_LABEL) &&
c.Labels.ContainsKey(RM_CREATED_LABEL) &&
c.Labels[DEPLOYMENT_NAME_LABEL] == deploymentName &&
c.Labels[DEPLOYMENT_GROUP_ID_LABEL] == deviceGroupId &&
c.Labels[RM_CREATED_LABEL] == bool.TrueString)))
.ReturnsAsync(newConfig);
// Act
var createdDeployment = await this.deployments.CreateAsync(depModel);
// Assert
Assert.False(string.IsNullOrEmpty(createdDeployment.Id));
Assert.Equal(deploymentName, createdDeployment.Name);
Assert.Equal(deviceGroupId, createdDeployment.DeviceGroupId);
Assert.Equal(priority, createdDeployment.Priority);
Assert.Equal(deviceGroupName, createdDeployment.DeviceGroupName);
Assert.Equal(packageName, createdDeployment.PackageName);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task FilterOutNonRmDeploymentsTest()
{
// Arrange
var configurations = new List<Configuration>
{
this.CreateConfiguration(0, true),
this.CreateConfiguration(1, false)
};
this.registry.Setup(r => r.GetConfigurationsAsync(20))
.ReturnsAsync(configurations);
// Act
var returnedDeployments = await this.deployments.ListAsync();
// Assert
Assert.Single(returnedDeployments.Items);
Assert.Equal("deployment0", returnedDeployments.Items[0].Name);
}
private Configuration CreateConfiguration(int idx, bool addCreatedByRmLabel)
{
var conf = new Configuration("test-config"+idx)
{
Labels = new Dictionary<string, string>()
{
{ DEPLOYMENT_NAME_LABEL, "deployment" + idx },
{ DEPLOYMENT_GROUP_ID_LABEL, "dvcGroupId" + idx }
}, Priority = 10
};
if (addCreatedByRmLabel)
{
conf.Labels.Add(RM_CREATED_LABEL, "true");
}
return conf;
}
}
}

Просмотреть файл

@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions;
using Moq;
using Services.Test.helpers;
using Xunit;
namespace Services.Test
{
public class DevicesTest
{
private readonly IDevices devices;
private readonly Mock<RegistryManager> registryMock;
private readonly string ioTHubHostName = "ioTHubHostName";
public DevicesTest()
{
this.registryMock = new Mock<RegistryManager>();
this.devices = new Devices(registryMock.Object, this.ioTHubHostName);
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("", "", true)]
[InlineData("asdf", "", true)]
[InlineData("", "qwer", true)]
[InlineData("asdf", "qwer", false)]
public async Task GetModuleTwinTest(string deviceId, string moduleId, bool throwsException)
{
if (throwsException)
{
// Act & Assert
await Assert.ThrowsAsync<InvalidInputException>(async () =>
await this.devices.GetModuleTwinAsync(deviceId, moduleId));
}
else
{
// Arrange
this.registryMock
.Setup(x => x.GetTwinAsync(deviceId, moduleId))
.ReturnsAsync(DevicesTest.CreateTestTwin(0));
// Act
var twinSvcModel = await this.devices.GetModuleTwinAsync(deviceId, moduleId);
// Assert
Assert.Equal("value0", twinSvcModel.ReportedProperties["test"].ToString());
Assert.Equal("value0", twinSvcModel.DesiredProperties["test"].ToString());
}
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("", 5)]
[InlineData("2", 5)]
[InlineData("6", 5)]
public async Task TwinByQueryContinuationTest(string continuationToken, int numResults)
{
this.registryMock
.Setup(x => x.CreateQuery(It.IsAny<string>()))
.Returns(new ResultQuery(numResults));
var queryResult = await this.devices.GetModuleTwinsByQueryAsync("", continuationToken);
Assert.Equal("continuationToken", queryResult.ContinuationToken);
var startIndex = string.IsNullOrEmpty(continuationToken) ? 0 : int.Parse(continuationToken);
var total = Math.Max(0, numResults - startIndex);
Assert.Equal(total, queryResult.Items.Count);
for (int i = 0; i < total; i++)
{
var expectedValue = "value" + (i + startIndex);
Assert.Equal(expectedValue, queryResult.Items[i].ReportedProperties["test"].ToString());
Assert.Equal(expectedValue, queryResult.Items[i].DesiredProperties["test"].ToString());
}
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("", "SELECT * FROM devices.modules")]
[InlineData("deviceId='test'", "SELECT * FROM devices.modules where deviceId='test'")]
public async Task GetTwinByQueryTest(string query, string queryToMatch)
{
this.registryMock
.Setup(x => x.CreateQuery(queryToMatch))
.Returns(new ResultQuery(3));
var queryResult = await this.devices.GetModuleTwinsByQueryAsync(query, "");
Assert.Equal("continuationToken", queryResult.ContinuationToken);
Assert.Equal(3, queryResult.Items.Count);
}
private static Twin CreateTestTwin(int valueToReport)
{
var twin = new Twin()
{
Properties = new TwinProperties()
};
twin.Properties.Reported = new TwinCollection("{\"test\":\"value" + valueToReport + "\"}");
twin.Properties.Desired = new TwinCollection("{\"test\":\"value" + valueToReport + "\"}");
return twin;
}
}
}

Просмотреть файл

@ -0,0 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Shared;
namespace Services.Test.helpers
{
public class ResultQuery : IQuery
{
private const string DEVICE_ID_KEY = "DeviceId";
private readonly List<Twin> results;
private readonly List<string> deviceQueryResults;
public ResultQuery(int numResults)
{
this.results = new List<Twin>();
this.deviceQueryResults = new List<string>();
for (int i = 0; i < numResults; i++)
{
this.results.Add(ResultQuery.CreateTestTwin(i));
this.deviceQueryResults.Add("{" + $"'{DEVICE_ID_KEY}':'device{i}'" + "}");
this.HasMoreResults = true;
}
}
public Task<IEnumerable<Twin>> GetNextAsTwinAsync()
{
this.HasMoreResults = false;
return Task.FromResult<IEnumerable<Twin>>(this.results);
}
public Task<QueryResponse<Twin>> GetNextAsTwinAsync(QueryOptions options)
{
this.HasMoreResults = false;
QueryResponse<Twin> resultResponse;
if (string.IsNullOrEmpty(options.ContinuationToken))
{
resultResponse = new QueryResponse<Twin>(this.results, "continuationToken");
}
else
{
var index = int.Parse(options.ContinuationToken);
var count = this.results.Count - index;
var continuedResults = new List<Twin>();
if (index < count)
{
continuedResults = this.results.GetRange(index, count);
}
resultResponse = new QueryResponse<Twin>(continuedResults, "continuationToken");
}
return Task.FromResult(resultResponse);
}
public Task<IEnumerable<DeviceJob>> GetNextAsDeviceJobAsync()
{
throw new System.NotImplementedException();
}
public Task<QueryResponse<DeviceJob>> GetNextAsDeviceJobAsync(QueryOptions options)
{
throw new System.NotImplementedException();
}
public Task<IEnumerable<JobResponse>> GetNextAsJobResponseAsync()
{
throw new System.NotImplementedException();
}
public Task<QueryResponse<JobResponse>> GetNextAsJobResponseAsync(QueryOptions options)
{
throw new System.NotImplementedException();
}
public Task<IEnumerable<string>> GetNextAsJsonAsync()
{
this.HasMoreResults = false;
return Task.FromResult(deviceQueryResults.AsEnumerable());
}
public Task<QueryResponse<string>> GetNextAsJsonAsync(QueryOptions options)
{
throw new System.NotImplementedException();
}
public bool HasMoreResults { get; set; }
private static Twin CreateTestTwin(int valueToReport)
{
var twin = new Twin()
{
Properties = new TwinProperties()
};
twin.Properties.Reported = new TwinCollection("{\"test\":\"value" + valueToReport + "\"}");
twin.Properties.Desired = new TwinCollection("{\"test\":\"value" + valueToReport + "\"}");
return twin;
}
}
}

Просмотреть файл

@ -0,0 +1,304 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Helpers;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Runtime;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
{
public interface IDeployments
{
Task<DeploymentServiceModel> CreateAsync(DeploymentServiceModel model);
Task<DeploymentServiceListModel> ListAsync();
Task<DeploymentServiceModel> GetAsync(string id, bool includeDeviceStatus);
Task DeleteAsync(string deploymentId);
}
[JsonConverter(typeof(StringEnumConverter))]
public enum DeploymentStatus
{
Pending, Successful, Failed
}
public class Deployments : IDeployments
{
private const int MAX_DEPLOYMENTS = 20;
private const string DEPLOYMENT_NAME_LABEL = "Name";
private const string DEPLOYMENT_GROUP_ID_LABEL = "DeviceGroupId";
private const string DEPLOYMENT_GROUP_NAME_LABEL = "DeviceGroupName";
private const string DEPLOYMENT_PACKAGE_NAME_LABEL = "PackageName";
private const string RM_CREATED_LABEL = "RMDeployment";
private const string DEVICE_GROUP_ID_PARAM = "deviceGroupId";
private const string DEVICE_GROUP_QUERY_PARAM = "deviceGroupQuery";
private const string NAME_PARAM = "name";
private const string PACKAGE_CONTENT_PARAM = "packageContent";
private const string PRIORITY_PARAM = "priority";
private const string DEVICE_ID_KEY = "DeviceId";
private const string EDGE_MANIFEST_SCHEMA = "schemaVersion";
private const string APPLIED_DEVICES_QUERY =
"select deviceId from devices.modules where moduleId = '$edgeAgent'" +
" and configurations.[[{0}]].status = 'Applied'";
private const string SUCCESSFUL_DEVICES_QUERY = APPLIED_DEVICES_QUERY +
" and properties.desired.$version = properties.reported.lastDesiredVersion" +
" and properties.reported.lastDesiredStatus.code = 200";
private const string FAILED_DEVICES_QUERY = APPLIED_DEVICES_QUERY +
" and properties.desired.$version = properties.reported.lastDesiredVersion" +
" and properties.reported.lastDesiredStatus.code != 200";
private RegistryManager registry;
private string ioTHubHostName;
private readonly ILogger log;
public Deployments(
IServicesConfig config,
ILogger logger)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
IoTHubConnectionHelper.CreateUsingHubConnectionString(config.IoTHubConnString, (conn) =>
{
this.registry = RegistryManager.CreateFromConnectionString(conn);
this.ioTHubHostName = IotHubConnectionStringBuilder.Create(conn).HostName;
});
this.log = logger;
}
public Deployments(
RegistryManager registry,
string ioTHubHostName)
{
this.registry = registry;
this.ioTHubHostName = ioTHubHostName;
}
/// <summary>
/// Schedules a deployment of the provided package, to the given group.
/// </summary>
/// <returns>Scheduled deployment</returns>
public async Task<DeploymentServiceModel> CreateAsync(DeploymentServiceModel model)
{
if (string.IsNullOrEmpty(model.DeviceGroupId))
{
throw new ArgumentNullException(DEVICE_GROUP_ID_PARAM);
}
if (string.IsNullOrEmpty(model.DeviceGroupQuery))
{
throw new ArgumentNullException(DEVICE_GROUP_QUERY_PARAM);
}
if (string.IsNullOrEmpty(model.Name))
{
throw new ArgumentNullException(NAME_PARAM);
}
if (string.IsNullOrEmpty(model.PackageContent))
{
throw new ArgumentNullException(PACKAGE_CONTENT_PARAM);
}
if (model.Priority < 0)
{
throw new ArgumentOutOfRangeException(PRIORITY_PARAM,
model.Priority,
"The priority provided should be 0 or greater");
}
var edgeConfiguration = this.CreateEdgeConfiguration(model);
// TODO: Add specific exception handling when exception types are exposed
// https://github.com/Azure/azure-iot-sdk-csharp/issues/649
return new DeploymentServiceModel(await this.registry.AddConfigurationAsync(edgeConfiguration));
}
/// <summary>
/// Retrieves all deployments that have been scheduled on the iothub.
/// Only deployments which were created by RM will be returned.
/// </summary>
/// <returns>All scheduled deployments with RMDeployment label</returns>
public async Task<DeploymentServiceListModel> ListAsync()
{
// TODO: Currently they only support 20 deployments
var deployments = await this.registry.GetConfigurationsAsync(MAX_DEPLOYMENTS);
if (deployments == null)
{
throw new ResourceNotFoundException($"No deployments found for {this.ioTHubHostName} hub.");
}
List<DeploymentServiceModel> serviceModelDeployments =
deployments.Where(this.CheckIfDeploymentWasMadeByRM)
.Select(config => new DeploymentServiceModel(config))
.OrderBy(conf => conf.Name)
.ToList();
return new DeploymentServiceListModel(serviceModelDeployments);
}
/// <summary>
/// Retrieve information on a single deployment given its id.
/// If includeDeviceStatus is included additional queries are created to retrieve the status of
/// the deployment per device.
/// </summary>
/// <returns>Deployment for the given id</returns>
public async Task<DeploymentServiceModel> GetAsync(string deploymentId, bool includeDeviceStatus = false)
{
if (string.IsNullOrEmpty(deploymentId))
{
throw new ArgumentNullException(nameof(deploymentId));
}
var deployment = await this.registry.GetConfigurationAsync(deploymentId);
if (deployment == null)
{
throw new ResourceNotFoundException($"Deployment with id {deploymentId} not found.");
}
if (!this.CheckIfDeploymentWasMadeByRM(deployment))
{
throw new ResourceNotSupportedException($"Deployment with id {deploymentId}" + @" was
created externally and therefore not supported");
}
return new DeploymentServiceModel(deployment)
{
DeploymentMetrics =
{
DeviceStatuses = includeDeviceStatus ? this.GetDeviceStatuses(deploymentId) : null
}
};
}
/// <summary>
/// Delete a given deployment by id.
/// </summary>
/// <returns></returns>
public async Task DeleteAsync(string deploymentId)
{
if(string.IsNullOrEmpty(deploymentId))
{
throw new ArgumentNullException(nameof(deploymentId));
}
await this.registry.RemoveConfigurationAsync(deploymentId);
}
private Configuration CreateEdgeConfiguration(DeploymentServiceModel model)
{
var deploymentId = Guid.NewGuid().ToString().ToLower();
var edgeConfiguration = new Configuration(deploymentId);
var packageEdgeConfiguration = JsonConvert.DeserializeObject<Configuration>(model.PackageContent);
edgeConfiguration.Content = packageEdgeConfiguration.Content;
var targetCondition = QueryConditionTranslator.ToQueryString(model.DeviceGroupQuery);
edgeConfiguration.TargetCondition = string.IsNullOrEmpty(targetCondition) ? "*" : targetCondition;
edgeConfiguration.Priority = model.Priority;
edgeConfiguration.ETag = string.Empty;
if(edgeConfiguration.Labels == null)
{
edgeConfiguration.Labels = new Dictionary<string, string>();
}
// Required labels
edgeConfiguration.Labels.Add(DEPLOYMENT_NAME_LABEL, model.Name);
edgeConfiguration.Labels.Add(DEPLOYMENT_GROUP_ID_LABEL, model.DeviceGroupId);
edgeConfiguration.Labels.Add(RM_CREATED_LABEL, bool.TrueString);
// Add optional labels
if (model.DeviceGroupName != null)
{
edgeConfiguration.Labels.Add(DEPLOYMENT_GROUP_NAME_LABEL, model.DeviceGroupName);
}
if (model.PackageName != null)
{
edgeConfiguration.Labels.Add(DEPLOYMENT_PACKAGE_NAME_LABEL, model.PackageName);
}
return edgeConfiguration;
}
private bool CheckIfDeploymentWasMadeByRM(Configuration conf)
{
return conf.Labels != null &&
conf.Labels.ContainsKey(RM_CREATED_LABEL) &&
bool.TryParse(conf.Labels[RM_CREATED_LABEL], out var res) && res;
}
private IDictionary<string, DeploymentStatus> GetDeviceStatuses(string deploymentId)
{
var appliedDevices = this.GetDevicesInQuery(APPLIED_DEVICES_QUERY, deploymentId);
var successfulDevices = this.GetDevicesInQuery(SUCCESSFUL_DEVICES_QUERY, deploymentId);
var failedDevices = this.GetDevicesInQuery(FAILED_DEVICES_QUERY, deploymentId);
var deviceWithStatus = new Dictionary<string, DeploymentStatus>();
foreach (var successfulDevice in successfulDevices)
{
deviceWithStatus.Add(successfulDevice, DeploymentStatus.Successful);
}
foreach (var failedDevice in failedDevices)
{
deviceWithStatus.Add(failedDevice, DeploymentStatus.Failed);
}
foreach (var device in appliedDevices)
{
if (!successfulDevices.Contains(device) && !failedDevices.Contains(device))
{
deviceWithStatus.Add(device, DeploymentStatus.Pending);
}
}
return deviceWithStatus;
}
private HashSet<string> GetDevicesInQuery(string hubQuery, string deploymentId)
{
var query = string.Format(hubQuery, deploymentId);
var queryResponse = this.registry.CreateQuery(query);
var deviceIds = new HashSet<string>();
try
{
while (queryResponse.HasMoreResults)
{
// TODO: Add pagination with queryOptions
var resultSet = queryResponse.GetNextAsJsonAsync();
foreach (var result in resultSet.Result)
{
var deviceId = JToken.Parse(result)[DEVICE_ID_KEY];
deviceIds.Add(deviceId.ToString());
}
}
}
catch (Exception ex)
{
this.log.Error($"Error getting status of devices in query {query}", () => new { ex.Message });
}
return deviceIds;
}
}
}

Просмотреть файл

@ -16,7 +16,6 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
{
public delegate Task<DevicePropertyServiceModel> DevicePropertyDelegate(DevicePropertyServiceModel model);
public interface IDevices
{
@ -26,12 +25,15 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
Task<DeviceServiceModel> CreateAsync(DeviceServiceModel toServiceModel);
Task<DeviceServiceModel> CreateOrUpdateAsync(DeviceServiceModel toServiceModel, DevicePropertyDelegate devicePropertyDelegate);
Task DeleteAsync(string id);
Task<TwinServiceModel> GetModuleTwinAsync(string deviceId, string moduleId);
Task<TwinServiceListModel> GetModuleTwinsByQueryAsync(string query, string continuationToken);
}
public class Devices : IDevices
{
private const int MAX_GET_LIST = 1000;
private const string QUERY_PREFIX = "SELECT * FROM devices";
private const string MODULE_QUERY_PREFIX = "SELECT * FROM devices.modules";
private RegistryManager registry;
private string ioTHubHostName;
@ -51,6 +53,12 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
});
}
public Devices(RegistryManager registry, string ioTHubHostName)
{
this.registry = registry;
this.ioTHubHostName = ioTHubHostName;
}
/// <summary>
/// Query devices
/// </summary>
@ -72,7 +80,10 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
// normally we need deviceTwins for all devices to show device list
var devices = await this.registry.GetDevicesAsync(MAX_GET_LIST);
var twins = await this.GetTwinByQueryAsync(query, continuationToken, MAX_GET_LIST);
var twins = await this.GetTwinByQueryAsync(QUERY_PREFIX,
query,
continuationToken,
MAX_GET_LIST);
// since deviceAsync does not support continuationToken for now, we need to ignore those devices which does not shown in twins
return new DeviceServiceListModel(devices
@ -179,16 +190,46 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
await this.registry.RemoveDeviceAsync(id);
}
public async Task<TwinServiceModel> GetModuleTwinAsync(string deviceId, string moduleId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
throw new InvalidInputException("A valid deviceId must be provided.");
}
if (string.IsNullOrWhiteSpace(moduleId))
{
throw new InvalidInputException("A valid moduleId must be provided.");
}
var twin = await this.registry.GetTwinAsync(deviceId, moduleId);
return new TwinServiceModel(twin);
}
public async Task<TwinServiceListModel> GetModuleTwinsByQueryAsync(string query,
string continuationToken)
{
var twins = await this.GetTwinByQueryAsync(MODULE_QUERY_PREFIX,
query,
continuationToken,
MAX_GET_LIST);
var result = twins.Result.Select(twin => new TwinServiceModel(twin)).ToList();
return new TwinServiceListModel(result, twins.ContinuationToken);
}
/// <summary>
/// Get twin result by query
/// </summary>
/// <param name="queryPrefix">The query prefix which selects devices or device modules</param>
/// <param name="query">The query without prefix</param>
/// <param name="continuationToken">The continuationToken</param>
/// <param name="nubmerOfResult">The max result</param>
/// <param name="numberOfResult">The max result</param>
/// <returns></returns>
private async Task<ResultWithContinuationToken<List<Twin>>> GetTwinByQueryAsync(string query, string continuationToken, int nubmerOfResult)
private async Task<ResultWithContinuationToken<List<Twin>>> GetTwinByQueryAsync(string queryPrefix,
string query, string continuationToken, int numberOfResult)
{
query = string.IsNullOrEmpty(query) ? QUERY_PREFIX : $"{QUERY_PREFIX} where {query}";
query = string.IsNullOrEmpty(query) ? queryPrefix : $"{queryPrefix} where {query}";
var twins = new List<Twin>();
@ -197,7 +238,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
QueryOptions options = new QueryOptions();
options.ContinuationToken = continuationToken;
while (twinQuery.HasMoreResults && twins.Count < nubmerOfResult)
while (twinQuery.HasMoreResults && twins.Count < numberOfResult)
{
var response = await twinQuery.GetNextAsTwinAsync(options);
options.ContinuationToken = response.ContinuationToken;

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions
{
/// <summary>
/// This exception is thrown when a client is requesting a resource that
/// was created outside of remote monitoring specifically by id.
/// </summary>
public class ResourceNotSupportedException : Exception
{
public ResourceNotSupportedException()
{
}
public ResourceNotSupportedException(string message) : base(message)
{
}
public ResourceNotSupportedException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

Просмотреть файл

@ -42,7 +42,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
Task<JobServiceModel> ScheduleTwinUpdateAsync(
string jobId,
string queryCondition,
DeviceTwinServiceModel twin,
TwinServiceModel twin,
DateTimeOffset startTimeUtc,
long maxExecutionTimeInSeconds);
}
@ -132,7 +132,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services
public async Task<JobServiceModel> ScheduleTwinUpdateAsync(
string jobId,
string queryCondition,
DeviceTwinServiceModel twin,
TwinServiceModel twin,
DateTimeOffset startTimeUtc,
long maxExecutionTimeInSeconds)
{

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.Devices;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
{
/// <summary>
/// Statistics exposed by configuration queries
/// </summary>
public class DeploymentMetrics
{
public IDictionary<string, long> Metrics { get; set; }
public IDictionary<string, DeploymentStatus> DeviceStatuses { get; set; }
public DeploymentMetrics(ConfigurationMetrics systemMetrics, ConfigurationMetrics customMetrics)
{
this.Metrics = new Dictionary<string, long>();
this.DeviceStatuses = new Dictionary<string, DeploymentStatus>();
if (systemMetrics?.Results?.Count > 0)
{
foreach (var pair in systemMetrics.Results)
{
this.Metrics.Add(pair);
}
}
if (customMetrics?.Results?.Count > 0)
{
foreach (var pair in customMetrics.Results)
{
this.Metrics.Add(pair);
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
{
public class DeploymentServiceListModel
{
public List<DeploymentServiceModel> Items { get; set; }
public DeploymentServiceListModel(List<DeploymentServiceModel> items)
{
this.Items = items;
}
}
}

Просмотреть файл

@ -0,0 +1,76 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Azure.Devices;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
{
public class DeploymentServiceModel
{
private const string DEPLOYMENT_NAME_LABEL = "Name";
private const string DEPLOYMENT_GROUP_ID_LABEL = "DeviceGroupId";
private const string DEPLOYMENT_GROUP_NAME_LABEL = "DeviceGroupName";
private const string DEPLOYMENT_PACKAGE_NAME_LABEL = "PackageName";
private const string RM_CREATED_LABEL = "RMDeployment";
public DateTime CreatedDateTimeUtc { get; set; }
public string Id { get; set; }
public DeploymentMetrics DeploymentMetrics { get; set; }
public string DeviceGroupId { get; set; }
public string DeviceGroupName { get; set; }
public string DeviceGroupQuery { get; set; }
public string Name {get; set; }
public string PackageContent { get; set; }
public string PackageName { get; set; }
public int Priority { get; set; }
public DeploymentType Type { get; set; }
public DeploymentServiceModel()
{
}
public DeploymentServiceModel(Configuration config)
{
if (string.IsNullOrEmpty(config.Id))
{
throw new ArgumentException($"Invalid deploymentId provided {config.Id}");
}
this.VerifyConfigurationLabel(config, DEPLOYMENT_NAME_LABEL);
this.VerifyConfigurationLabel(config, DEPLOYMENT_GROUP_ID_LABEL);
this.VerifyConfigurationLabel(config, RM_CREATED_LABEL);
this.Id = config.Id;
this.Name = config.Labels[DEPLOYMENT_NAME_LABEL];
this.CreatedDateTimeUtc = config.CreatedTimeUtc;
this.DeviceGroupId = config.Labels[DEPLOYMENT_GROUP_ID_LABEL];
if (config.Labels.ContainsKey(DEPLOYMENT_GROUP_NAME_LABEL))
{
this.DeviceGroupName = config.Labels[DEPLOYMENT_GROUP_NAME_LABEL];
}
if (config.Labels.ContainsKey(DEPLOYMENT_PACKAGE_NAME_LABEL))
{
this.PackageName = config.Labels[DEPLOYMENT_PACKAGE_NAME_LABEL];
}
this.Priority = config.Priority;
this.Type = DeploymentType.EdgeManifest;
this.DeploymentMetrics = new DeploymentMetrics(config.SystemMetrics, config.Metrics);
}
private void VerifyConfigurationLabel(Configuration config, string labelName)
{
if (!config.Labels.ContainsKey(labelName))
{
throw new ArgumentException($"Configuration is missing necessary label {labelName}");
}
}
}
public enum DeploymentType {
EdgeManifest
}
}

Просмотреть файл

@ -15,7 +15,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
public bool Connected { get; set; }
public bool Enabled { get; set; }
public DateTime LastStatusUpdated { get; set; }
public DeviceTwinServiceModel Twin { get; set; }
public TwinServiceModel Twin { get; set; }
public string IoTHubHostName { get; set; }
public AuthenticationMechanismServiceModel Authentication { get; set; }
@ -27,7 +27,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
bool connected,
bool enabled,
DateTime lastStatusUpdated,
DeviceTwinServiceModel twin,
TwinServiceModel twin,
AuthenticationMechanismServiceModel authentication,
string ioTHubHostName)
{
@ -43,7 +43,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
this.Authentication = authentication;
}
public DeviceServiceModel(Device azureDevice, DeviceTwinServiceModel twin, string ioTHubHostName) :
public DeviceServiceModel(Device azureDevice, TwinServiceModel twin, string ioTHubHostName) :
this(
etag: azureDevice.ETag,
id: azureDevice.Id,
@ -59,7 +59,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
}
public DeviceServiceModel(Device azureDevice, Twin azureTwin, string ioTHubHostName) :
this(azureDevice, new DeviceTwinServiceModel(azureTwin), ioTHubHostName)
this(azureDevice, new TwinServiceModel(azureTwin), ioTHubHostName)
{
}

Просмотреть файл

@ -27,7 +27,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
public MethodParameterServiceModel MethodParameter { get; set; }
public DeviceTwinServiceModel UpdateTwin { get; set; }
public TwinServiceModel UpdateTwin { get; set; }
public string FailureReason { get; set; }
@ -89,7 +89,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
if (jobResponse.UpdateTwin != null)
{
this.UpdateTwin = new DeviceTwinServiceModel(jobResponse.UpdateTwin);
this.UpdateTwin = new TwinServiceModel(jobResponse.UpdateTwin);
}
this.FailureReason = jobResponse.FailureReason;

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
{
public class TwinServiceListModel
{
public string ContinuationToken { get; set; }
public List<TwinServiceModel> Items { get; set; }
public TwinServiceListModel(IEnumerable<TwinServiceModel> twins, string continuationToken = null)
{
this.ContinuationToken = continuationToken;
this.Items = new List<TwinServiceModel>(twins);
}
}
}

Просмотреть файл

@ -7,44 +7,68 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
{
public class DeviceTwinServiceModel
public class TwinServiceModel
{
public string ETag { get; set; }
public string DeviceId { get; set; }
public string ModuleId { get; set; }
public bool IsEdgeDevice { get; set; }
public bool IsSimulated { get; set; }
public Dictionary<string, JToken> DesiredProperties { get; set; }
public Dictionary<string, JToken> ReportedProperties { get; set; }
public Dictionary<string, JToken> Tags { get; set; }
public DeviceTwinServiceModel()
public TwinServiceModel()
{
}
public DeviceTwinServiceModel(
public TwinServiceModel(
string etag,
string deviceId,
Dictionary<string, JToken> desiredProperties,
Dictionary<string, JToken> reportedProperties,
Dictionary<string, JToken> tags,
bool isSimulated)
bool isSimulated) : this(
etag: etag,
deviceId: deviceId,
desiredProperties: desiredProperties,
reportedProperties: reportedProperties,
tags: tags,
isSimulated: isSimulated,
isEdgeDevice: false
)
{
}
public TwinServiceModel(
string etag,
string deviceId,
Dictionary<string, JToken> desiredProperties,
Dictionary<string, JToken> reportedProperties,
Dictionary<string, JToken> tags,
bool isSimulated,
bool isEdgeDevice)
{
this.ETag = etag;
this.DeviceId = deviceId;
this.DesiredProperties = desiredProperties;
this.ReportedProperties = reportedProperties;
this.Tags = tags;
this.IsEdgeDevice = isEdgeDevice;
this.IsSimulated = isSimulated;
}
public DeviceTwinServiceModel(Twin twin)
public TwinServiceModel(Twin twin)
{
if (twin != null)
{
this.ETag = twin.ETag;
this.DeviceId = twin.DeviceId;
this.ModuleId = twin.ModuleId;
this.Tags = TwinCollectionToDictionary(twin.Tags);
this.DesiredProperties = TwinCollectionToDictionary(twin.Properties.Desired);
this.ReportedProperties = TwinCollectionToDictionary(twin.Properties.Reported);
this.IsEdgeDevice = twin.Capabilities?.IotEdge ?? false;
this.IsSimulated = this.Tags.ContainsKey("IsSimulated") && this.Tags["IsSimulated"].ToString() == "Y";
}
}
@ -61,7 +85,11 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models
{
ETag = this.ETag,
Tags = DictionaryToTwinCollection(this.Tags),
Properties = properties
Properties = properties,
Capabilities = this.IsEdgeDevice ? new DeviceCapabilities()
{
IotEdge = this.IsEdgeDevice
} : null
};
}

Просмотреть файл

@ -18,7 +18,6 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.Services.Runtime
public string IoTHubConnString { get; set; }
public string StorageAdapterApiUrl { get; set; }
public string UserManagementApiUrl { get; set; }
public string ConfigServiceUri { get; set; }
public string DevicePropertiesWhiteList { get; set; }
public long DevicePropertiesTTL { get; set; }
public long DevicePropertiesRebuildTimeout { get; set; }

Просмотреть файл

@ -6,7 +6,7 @@
<RootNamespace>Microsoft.Azure.IoTSolutions.IotHubManager.Services</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices" Version="1.4.1" />
<PackageReference Include="Microsoft.Azure.Devices" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="2.0.0" />
</ItemGroup>

Просмотреть файл

@ -0,0 +1,207 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Controllers;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models;
using Moq;
using WebService.Test.helpers;
using Xunit;
namespace WebService.Test.v1.Controllers
{
public class DeploymentsControllerTest
{
private readonly DeploymentsController deploymentsController;
private readonly Mock<IDeployments> deploymentsMock;
private const string DEPLOYMENT_NAME = "depname";
private const string DEVICE_GROUP_ID = "dvcGroupId";
private const string DEVICE_GROUP_NAME = "dvcGroupName";
private const string DEVICE_GROUP_QUERY = "dvcGroupQuery";
private const string PACKAGE_CONTENT = "{}";
private const string PACKAGE_NAME = "packageName";
private const string DEPLOYMENT_ID = "dvcGroupId-packageId";
private const int PRIORITY = 10;
public DeploymentsControllerTest()
{
this.deploymentsMock = new Mock<IDeployments>();
this.deploymentsController = new DeploymentsController(this.deploymentsMock.Object);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task GetDeploymentTest()
{
// Arrange
this.deploymentsMock.Setup(x => x.GetAsync(DEPLOYMENT_ID, false)).ReturnsAsync(new DeploymentServiceModel()
{
Name = DEPLOYMENT_NAME,
DeviceGroupId = DEVICE_GROUP_ID,
DeviceGroupName = DEVICE_GROUP_NAME,
DeviceGroupQuery = DEVICE_GROUP_QUERY,
PackageContent = PACKAGE_CONTENT,
PackageName = PACKAGE_NAME,
Priority = PRIORITY,
Id = DEPLOYMENT_ID,
Type = DeploymentType.EdgeManifest,
CreatedDateTimeUtc = DateTime.UtcNow
});
// Act
var result = await this.deploymentsController.GetAsync(DEPLOYMENT_ID);
// Assert
Assert.Equal(DEPLOYMENT_ID, result.DeploymentId);
Assert.Equal(DEPLOYMENT_NAME, result.Name);
Assert.Equal(PACKAGE_CONTENT, result.PackageContent);
Assert.Equal(PACKAGE_NAME, result.PackageName);
Assert.Equal(DEVICE_GROUP_ID, result.DeviceGroupId);
Assert.Equal(DEVICE_GROUP_NAME, result.DeviceGroupName);
Assert.Equal(PRIORITY, result.Priority);
Assert.Equal(DeploymentType.EdgeManifest, result.Type);
Assert.True((DateTimeOffset.UtcNow - result.CreatedDateTimeUtc).TotalSeconds < 5);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public async Task VerifyGroupAndPackageNameLabelsTest()
{
// Arrange
this.deploymentsMock.Setup(x => x.GetAsync(DEPLOYMENT_ID, false)).ReturnsAsync(new DeploymentServiceModel()
{
Name = DEPLOYMENT_NAME,
DeviceGroupId = DEVICE_GROUP_ID,
DeviceGroupName = DEVICE_GROUP_NAME,
DeviceGroupQuery = DEVICE_GROUP_QUERY,
PackageContent = PACKAGE_CONTENT,
Priority = PRIORITY,
Id = DEPLOYMENT_ID,
Type = DeploymentType.EdgeManifest,
CreatedDateTimeUtc = DateTime.UtcNow
});
// Act
var result = await this.deploymentsController.GetAsync(DEPLOYMENT_ID);
// Assert
Assert.Equal(DEPLOYMENT_ID, result.DeploymentId);
Assert.Equal(DEPLOYMENT_NAME, result.Name);
Assert.Equal(PACKAGE_CONTENT, result.PackageContent);
Assert.Equal(DEVICE_GROUP_ID, result.DeviceGroupId);
Assert.Equal(PRIORITY, result.Priority);
Assert.Equal(DeploymentType.EdgeManifest, result.Type);
Assert.True((DateTimeOffset.UtcNow - result.CreatedDateTimeUtc).TotalSeconds < 5);
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData(0)]
[InlineData(1)]
[InlineData(5)]
public async Task GetDeploymentsTest(int numDeployments)
{
// Arrange
var deploymentsList = new List<DeploymentServiceModel>();
for (var i = 0; i < numDeployments; i++)
{
deploymentsList.Add(new DeploymentServiceModel()
{
Name = DEPLOYMENT_NAME + i,
DeviceGroupId = DEVICE_GROUP_ID + i,
DeviceGroupQuery = DEVICE_GROUP_QUERY + i,
PackageContent = PACKAGE_CONTENT + i,
Priority = PRIORITY + i,
Id = DEPLOYMENT_ID + i,
Type = DeploymentType.EdgeManifest,
CreatedDateTimeUtc = DateTime.UtcNow
});
}
this.deploymentsMock.Setup(x => x.ListAsync()).ReturnsAsync(
new DeploymentServiceListModel(deploymentsList)
);
// Act
var results = await this.deploymentsController.GetAsync();
// Assert
Assert.Equal(numDeployments, results.Items.Count);
for (var i = 0; i < numDeployments; i++)
{
var result = results.Items[i];
Assert.Equal(DEPLOYMENT_ID + i, result.DeploymentId);
Assert.Equal(DEPLOYMENT_NAME + i, result.Name);
Assert.Equal(DEVICE_GROUP_QUERY + i, result.DeviceGroupQuery);
Assert.Equal(DEVICE_GROUP_ID + i, result.DeviceGroupId);
Assert.Equal(PACKAGE_CONTENT + i, result.PackageContent);
Assert.Equal(PRIORITY + i, result.Priority);
Assert.Equal(DeploymentType.EdgeManifest, result.Type);
Assert.True((DateTimeOffset.UtcNow - result.CreatedDateTimeUtc).TotalSeconds < 5);
}
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("depName", "dvcGroupId", "dvcQuery", "pkgContent", 10, false)]
[InlineData("", "dvcGroupId", "dvcQuery", "pkgContent", 10, true)]
[InlineData("depName", "", "dvcQuery", "pkgContent", 10, true)]
[InlineData("depName", "dvcGroupId", "", "pkgContent", 10, true)]
[InlineData("depName", "dvcGroupId", "dvcQuery", "", 10, true)]
[InlineData("depName", "dvcGroupId", "dvcQuery", "pkgContent", -1, true)]
public async Task PostDeploymentTest(string name, string deviceGroupId,
string deviceGroupQuery, string packageContent,
int priority, bool throwsException)
{
// Arrange
var deploymentId = "test-deployment";
this.deploymentsMock.Setup(x => x.CreateAsync(Match.Create<DeploymentServiceModel>(model =>
model.DeviceGroupId == deviceGroupId &&
model.PackageContent == packageContent &&
model.Priority == priority &&
model.Name == name &&
model.Type == DeploymentType.EdgeManifest)))
.ReturnsAsync(new DeploymentServiceModel()
{
Name = name,
DeviceGroupId = deviceGroupId,
DeviceGroupQuery = deviceGroupQuery,
PackageContent = packageContent,
Priority = priority,
Id = deploymentId,
Type = DeploymentType.EdgeManifest,
CreatedDateTimeUtc = DateTime.UtcNow
});
var depApiModel = new DeploymentApiModel()
{
Name = name,
DeviceGroupId = deviceGroupId,
DeviceGroupQuery = deviceGroupQuery,
PackageContent = packageContent,
Type = DeploymentType.EdgeManifest,
Priority = priority
};
// Act
if (throwsException)
{
await Assert.ThrowsAsync<InvalidInputException>(async () => await this.deploymentsController.PostAsync(depApiModel));
}
else
{
var result = await this.deploymentsController.PostAsync(depApiModel);
// Assert
Assert.Equal(deploymentId, result.DeploymentId);
Assert.Equal(name, result.Name);
Assert.Equal(deviceGroupId, result.DeviceGroupId);
Assert.Equal(deviceGroupQuery, result.DeviceGroupQuery);
Assert.Equal(packageContent, result.PackageContent);
Assert.Equal(priority, result.Priority);
Assert.Equal(DeploymentType.EdgeManifest, result.Type);
Assert.True((DateTimeOffset.UtcNow - result.CreatedDateTimeUtc).TotalSeconds < 5);
}
}
}
}

Просмотреть файл

@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Controllers;
using Microsoft.Extensions.Primitives;
using Moq;
using Newtonsoft.Json.Linq;
using WebService.Test.helpers;
using Xunit;
namespace WebService.Test.v1.Controllers
{
public class ModulesControllerTest
{
private readonly ModulesController modulesController;
private readonly Mock<IDevices> devicesMock;
private readonly HttpContext httpContext;
private const string CONTINUATION_TOKEN_NAME = "x-ms-continuation";
public ModulesControllerTest()
{
this.devicesMock = new Mock<IDevices>();
this.httpContext = new DefaultHttpContext();
this.modulesController = new ModulesController(this.devicesMock.Object)
{
ControllerContext = new ControllerContext()
{
HttpContext = this.httpContext
}
};
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("", "", true)]
[InlineData("deviceId", "", true)]
[InlineData("", "moduleId", true)]
[InlineData("deviceId", "moduleId", false)]
public async Task GetSingleModuleTwinTest(string deviceId, string moduleId, bool throwsException)
{
if (throwsException)
{
await Assert.ThrowsAsync<InvalidInputException>(async () =>
await this.modulesController.GetModuleTwinAsync(deviceId, moduleId));
}
else
{
// Arrange
var twinResult = ModulesControllerTest.CreateTestTwin(deviceId, moduleId);
this.devicesMock.Setup(x => x.GetModuleTwinAsync(deviceId, moduleId))
.ReturnsAsync(twinResult);
// Act
var module = await this.modulesController.GetModuleTwinAsync(deviceId, moduleId);
// Assert
Assert.Equal(moduleId, module.ModuleId);
Assert.Equal(deviceId, module.DeviceId);
Assert.Equal("v2", module.Desired["version"]);
Assert.Equal("v1", module.Reported["version"]);
}
}
[Theory, Trait(Constants.TYPE, Constants.UNIT_TEST)]
[InlineData("", "")]
[InlineData("my module query", "continuationToken")]
public async Task GetModuleTwinsTest(string query, string continuationToken)
{
const string resultToken = "nextToken";
var twinList = new List<TwinServiceModel>() {ModulesControllerTest.CreateTestTwin("d", "m")};
var twins = new TwinServiceListModel(twinList, resultToken);
this.devicesMock.Setup(x => x.GetModuleTwinsByQueryAsync(query, continuationToken))
.ReturnsAsync(twins);
this.httpContext.Request.Headers.Add(CONTINUATION_TOKEN_NAME,
new StringValues(continuationToken));
// Act
var moduleTwins = await this.modulesController.GetModuleTwinsAsync(query);
// Assert
var moduleTwin = moduleTwins.Items[0];
Assert.Equal("d", moduleTwin.DeviceId);
Assert.Equal("m", moduleTwin.ModuleId);
Assert.Equal(resultToken, moduleTwins.ContinuationToken);
Assert.Equal("v2", moduleTwin.Desired["version"]);
Assert.Equal("v1", moduleTwin.Reported["version"]);
}
private static TwinServiceModel CreateTestTwin(string deviceId, string moduleId)
{
return new TwinServiceModel()
{
DeviceId = deviceId,
ModuleId = moduleId,
DesiredProperties = new Dictionary<string, JToken>()
{
{ "version", JToken.Parse("'v2'") }
},
ReportedProperties = new Dictionary<string, JToken>()
{
{ "version", JToken.Parse("'v1'") }
}
};
}
}
}

Просмотреть файл

@ -79,7 +79,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.Runtime
DevicePropertiesTTL = configData.GetInt(DEVICE_PROPERTIES_TTL_KEY),
DevicePropertiesRebuildTimeout = configData.GetInt(DEVICE_PROPERTIES_REBUILD_TIMEOUT_KEY),
StorageAdapterApiUrl = configData.GetString(STORAGE_ADAPTER_URL_KEY),
UserManagementApiUrl = configData.GetString(USER_MANAGEMENT_URL_KEY),
UserManagementApiUrl = configData.GetString(USER_MANAGEMENT_URL_KEY)
};
this.ClientAuthConfig = new ClientAuthConfig

Просмотреть файл

@ -0,0 +1,81 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Filters;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Controllers
{
[Route(Version.PATH + "/[controller]"), ExceptionsFilter]
public class DeploymentsController : Controller
{
private readonly IDeployments deployments;
public DeploymentsController(IDeployments deployments)
{
this.deployments = deployments;
}
/// <summary>Create a deployment targeting a particular group</summary>
/// <param name="deployment">Deployment information</param>
/// <returns>Deployment information and initial success metrics</returns>
[HttpPost]
[Authorize("CreateDeployments")]
public async Task<DeploymentApiModel> PostAsync([FromBody] DeploymentApiModel deployment)
{
if (string.IsNullOrWhiteSpace(deployment.Name))
{
throw new InvalidInputException("Name must be provided");
}
if (string.IsNullOrWhiteSpace(deployment.DeviceGroupId))
{
throw new InvalidInputException("DeviceGroupId must be provided");
}
if (string.IsNullOrWhiteSpace(deployment.DeviceGroupQuery))
{
throw new InvalidInputException("DeviceGroupQuery must be provided");
}
if (string.IsNullOrWhiteSpace(deployment.PackageContent))
{
throw new InvalidInputException("PackageContent must be provided");
}
if (deployment.Priority < 0)
{
throw new InvalidInputException($"Invalid priority provided of {deployment.Priority}. " +
"It must be non-negative");
}
return new DeploymentApiModel(await this.deployments.CreateAsync(deployment.ToServiceModel()));
}
[HttpGet]
public async Task<DeploymentListApiModel> GetAsync()
{
return new DeploymentListApiModel(await this.deployments.ListAsync());
}
/// <summary>Get one deployment</summary>
/// <param name="id">Deployment id</param>
/// <param name="includeDeviceStatus">Whether to retrieve additional details regarding device status</param>
/// <returns>Deployment information with metrics</returns>
[HttpGet("{id}")]
public async Task<DeploymentApiModel> GetAsync(string id, [FromQuery] bool includeDeviceStatus = false)
{
return new DeploymentApiModel(await this.deployments.GetAsync(id, includeDeviceStatus));
}
[HttpDelete("{id}")]
[Authorize("DeleteDeployments")]
public async Task DeleteAsync(string id)
{
await this.deployments.DeleteAsync(id);
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Filters;
using Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Controllers
{
[Route(Version.PATH + "/[controller]"), ExceptionsFilter]
public class ModulesController : Controller
{
private const string CONTINUATION_TOKEN_NAME = "x-ms-continuation";
private readonly IDevices devices;
public ModulesController(IDevices devices)
{
this.devices = devices;
}
/// <summary>Retrieve module twin properties based on provided query</summary>
/// <param name="query">Where clause of IoTHub query</param>
/// <returns>List of module twins</returns>
[HttpGet]
public async Task<TwinPropertiesListApiModel> GetModuleTwinsAsync([FromQuery] string query)
{
string continuationToken = string.Empty;
if (this.Request.Headers.ContainsKey(CONTINUATION_TOKEN_NAME))
{
continuationToken = this.Request.Headers[CONTINUATION_TOKEN_NAME].FirstOrDefault();
}
return new TwinPropertiesListApiModel(
await this.devices.GetModuleTwinsByQueryAsync(query, continuationToken));
}
/// <summary>Retrieve module twin properties. Query in body of post request</summary>
/// <param name="query">Where clause of IoTHub query</param>
/// <returns>List of module twins</returns>
[HttpPost("query")]
public async Task<TwinPropertiesListApiModel> QueryModuleTwinsAsync([FromBody] string query)
{
return await this.GetModuleTwinsAsync(query);
}
/// <summary>Get module information for a device</summary>
/// <param name="deviceId">Device Id</param>
/// <param name="moduleId">Module Id</param>
/// <returns>Device information</returns>
[HttpGet("{deviceId}/{moduleId}")]
public async Task<TwinPropertiesApiModel> GetModuleTwinAsync(string deviceId, string moduleId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
throw new InvalidInputException("deviceId must be provided");
}
if (string.IsNullOrWhiteSpace(moduleId))
{
throw new InvalidInputException("moduleId must be provided");
}
var twin = await this.devices.GetModuleTwinAsync(deviceId, moduleId);
return new TwinPropertiesApiModel(twin.DesiredProperties, twin.ReportedProperties,
deviceId, moduleId);
}
}
}

Просмотреть файл

@ -0,0 +1,99 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
{
public class DeploymentApiModel
{
[JsonProperty(PropertyName = "Id")]
public string DeploymentId { get; set; }
[JsonProperty(PropertyName = "Name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "CreatedDateTimeUtc")]
public DateTime CreatedDateTimeUtc { get; set; }
[JsonProperty(PropertyName = "DeviceGroupId")]
public string DeviceGroupId { get; set; }
[JsonProperty(PropertyName = "DeviceGroupName")]
public string DeviceGroupName { get; set; }
[JsonProperty(PropertyName = "DeviceGroupQuery")]
public string DeviceGroupQuery { get; set; }
[JsonProperty(PropertyName = "PackageContent")]
public string PackageContent { get; set; }
[JsonProperty(PropertyName = "PackageName")]
public string PackageName { get; set; }
[JsonProperty(PropertyName = "Priority")]
public int Priority { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(PropertyName = "Type")]
public DeploymentType Type { get; set; }
[JsonProperty(PropertyName = "Metrics", NullValueHandling = NullValueHandling.Ignore)]
public DeploymentMetricsApiModel Metrics { get; set; }
public DeploymentApiModel()
{
}
public DeploymentApiModel(DeploymentServiceModel serviceModel)
{
this.CreatedDateTimeUtc = serviceModel.CreatedDateTimeUtc;
this.DeploymentId = serviceModel.Id;
this.DeviceGroupId = serviceModel.DeviceGroupId;
this.DeviceGroupName = serviceModel.DeviceGroupName;
this.DeviceGroupQuery = serviceModel.DeviceGroupQuery;
this.Name = serviceModel.Name;
this.PackageContent = serviceModel.PackageContent;
this.PackageName = serviceModel.PackageName;
this.Priority = serviceModel.Priority;
this.Type = serviceModel.Type;
this.Metrics = new DeploymentMetricsApiModel(serviceModel.DeploymentMetrics)
{
DeviceStatuses = serviceModel.DeploymentMetrics?.DeviceStatuses
};
}
public DeploymentApiModel(DeploymentServiceModel serviceModel,
IDictionary<string, DeploymentStatus> deviceStatuses)
{
this.CreatedDateTimeUtc = serviceModel.CreatedDateTimeUtc;
this.DeploymentId = serviceModel.Id;
this.DeviceGroupId = serviceModel.DeviceGroupId;
this.Name = serviceModel.Name;
this.Priority = serviceModel.Priority;
this.Type = serviceModel.Type;
this.Metrics = new DeploymentMetricsApiModel(serviceModel.DeploymentMetrics)
{
DeviceStatuses = deviceStatuses
};
}
public DeploymentServiceModel ToServiceModel()
{
return new DeploymentServiceModel() {
DeviceGroupId = this.DeviceGroupId,
DeviceGroupName = this.DeviceGroupName,
DeviceGroupQuery = this.DeviceGroupQuery,
Name = this.Name,
PackageContent = this.PackageContent,
PackageName = this.PackageName,
Priority = this.Priority,
Type = this.Type
};
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
{
public class DeploymentListApiModel
{
[JsonProperty(PropertyName = "Items")]
public List<DeploymentApiModel> Items { get; set; }
public DeploymentListApiModel()
{
}
public DeploymentListApiModel(DeploymentServiceListModel deployments)
{
this.Items = new List<DeploymentApiModel>();
deployments.Items.ForEach(deployment => this.Items.Add(new DeploymentApiModel(deployment)));
}
}
}

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
{
public class DeploymentMetricsApiModel
{
private const string APPLIED_METRICS_KEY = "appliedCount";
private const string TARGETED_METRICS_KEY = "targetedCount";
private const string SUCCESSFUL_METRICS_KEY = "reportedSuccessfulCount";
private const string FAILED_METRICS_KEY = "reportedFailedCount";
[JsonProperty(PropertyName = "AppliedCount")]
public long AppliedCount { get; set; }
[JsonProperty(PropertyName = "FailedCount")]
public long FailedCount { get; set; }
[JsonProperty(PropertyName = "SucceededCount")]
public long SucceededCount { get; set; }
[JsonProperty(PropertyName = "TargetedCount")]
public long TargetedCount { get; set; }
[JsonProperty(PropertyName = "DeviceStatuses")]
public IDictionary<string, DeploymentStatus> DeviceStatuses { get; set; }
public DeploymentMetricsApiModel()
{
}
public DeploymentMetricsApiModel(DeploymentMetrics metricsServiceModel)
{
if (metricsServiceModel == null) return;
var metrics = metricsServiceModel.Metrics;
this.AppliedCount = metrics.TryGetValue(APPLIED_METRICS_KEY, out var value) ? value : 0;
this.TargetedCount = metrics.TryGetValue(TARGETED_METRICS_KEY, out value) ? value : 0;
this.SucceededCount = metrics.TryGetValue(SUCCESSFUL_METRICS_KEY, out value) ? value : 0;
this.FailedCount = metrics.TryGetValue(FAILED_METRICS_KEY, out value) ? value : 0;
}
}
}

Просмотреть файл

@ -48,6 +48,9 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
[JsonProperty(PropertyName = "Tags", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, JToken> Tags { get; set; }
[JsonProperty(PropertyName = "IsEdgeDevice")]
public bool IsEdgeDevice { get; set; }
[JsonProperty(PropertyName = "IsSimulated", NullValueHandling = NullValueHandling.Ignore)]
public bool IsSimulated { get; set; }
@ -77,6 +80,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
this.ETag = $"{this.ETag}|{device.Twin.ETag}";
this.Properties = new TwinPropertiesApiModel(device.Twin.DesiredProperties, device.Twin.ReportedProperties);
this.Tags = device.Twin.Tags;
this.IsEdgeDevice = device.Twin.IsEdgeDevice;
this.IsSimulated = device.Twin.IsSimulated;
}
}
@ -117,14 +121,15 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
public DeviceServiceModel ToServiceModel()
{
var twinModel = new DeviceTwinServiceModel
var twinModel = new TwinServiceModel
(
etag: this.TwinEtag,
deviceId: this.Id,
desiredProperties: this.Properties?.Desired,
reportedProperties: this.Properties?.Reported,
tags: this.Tags,
isSimulated: this.IsSimulated
isSimulated: this.IsSimulated,
isEdgeDevice: this.IsEdgeDevice
);
return new DeviceServiceModel

Просмотреть файл

@ -30,7 +30,7 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
this.Properties = new TwinPropertiesApiModel();
}
public JobUpdateTwinApiModel(string deviceId, DeviceTwinServiceModel deviceTwin)
public JobUpdateTwinApiModel(string deviceId, TwinServiceModel deviceTwin)
{
if (deviceTwin != null)
{
@ -42,9 +42,9 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
}
}
public DeviceTwinServiceModel ToServiceModel()
public TwinServiceModel ToServiceModel()
{
return new DeviceTwinServiceModel
return new TwinServiceModel
(
etag: this.ETag,
deviceId: this.DeviceId,

Просмотреть файл

@ -14,16 +14,30 @@ namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
[JsonProperty(PropertyName = "Desired", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, JToken> Desired { get; set; }
[JsonProperty(PropertyName = "DeviceId", NullValueHandling = NullValueHandling.Ignore)]
public string DeviceId { get; set; }
[JsonProperty(PropertyName = "ModuleId", NullValueHandling = NullValueHandling.Ignore)]
public string ModuleId { get; set; }
public TwinPropertiesApiModel()
{
this.Reported = new Dictionary<string, JToken>();
this.Desired = new Dictionary<string, JToken>();
}
public TwinPropertiesApiModel(Dictionary<string, JToken> desired, Dictionary<string, JToken> reported)
public TwinPropertiesApiModel(Dictionary<string, JToken> desired, Dictionary<string, JToken> reported) :
this(desired, reported, string.Empty, string.Empty)
{
}
public TwinPropertiesApiModel(Dictionary<string, JToken> desired, Dictionary<string, JToken> reported,
string deviceId, string moduleId)
{
this.Desired = desired;
this.Reported = reported;
this.DeviceId = deviceId;
this.ModuleId = moduleId;
}
}
}

Просмотреть файл

@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Azure.IoTSolutions.IotHubManager.Services.Models;
using Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.IotHubManager.WebService.v1.Models
{
public class TwinPropertiesListApiModel
{
[JsonProperty(PropertyName = "$metadata")]
public Dictionary<string, string> Metadata => new Dictionary<string, string>
{
{ "$type", "DeviceList;" + Version.NUMBER },
{ "$uri", "/" + Version.PATH + "/devices" }
};
[JsonProperty(PropertyName = "ContinuationToken")]
public string ContinuationToken { get; set; }
[JsonProperty(PropertyName = "Items")]
public List<TwinPropertiesApiModel> Items { get; set; }
public TwinPropertiesListApiModel()
{
}
public TwinPropertiesListApiModel(TwinServiceListModel twins)
{
this.Items = new List<TwinPropertiesApiModel>();
this.ContinuationToken = twins.ContinuationToken;
foreach (var t in twins.Items)
{
this.Items.Add(new TwinPropertiesApiModel(t.DesiredProperties, t.ReportedProperties,
t.DeviceId, t.ModuleId));
}
}
}
}