Skip to content

Commit

Permalink
Add Greengrass automagic deployment + Greengrass create and delete fr…
Browse files Browse the repository at this point in the history
…om Portal (#2123)

* Create DeviceCredentials, CertificateCredentials and rename EnrollmentCredentials to SymmetricCredentials

* Add certificate generations steps

* Fix #2074  - add greengrass policies

* fix #2074 - Create Greengrass enrollement script

* Fix unit tests

* Add unit tests to cover AWS provider

---------

Co-authored-by: ropiteauxi <ivan.ropiteaux@cgi.com>
  • Loading branch information
kbeaugrand and ivanropiteaux committed Jun 20, 2023
1 parent 05837c0 commit 60ba3cf
Show file tree
Hide file tree
Showing 56 changed files with 5,111 additions and 3,167 deletions.
46 changes: 29 additions & 17 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/dotnet/.devcontainer/base.Dockerfile

# [Choice] .NET version: 6.0, 3.1, 6.0-bullseye, 3.1-bullseye, 6.0-focal, 3.1-focal
ARG VARIANT="7.0-bullseye-slim"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:dev-${VARIANT}

# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends moby-engine moby-cli moby-build moby-run

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/dotnet/.devcontainer/base.Dockerfile

# [Choice] .NET version: 6.0, 3.1, 6.0-bullseye, 3.1-bullseye, 6.0-focal, 3.1-focal
ARG VARIANT="7.0-bullseye-slim"
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:dev-${VARIANT}

# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends moby-engine moby-cli moby-build moby-run

# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

RUN sudo apt-get update && \
sudo apt-get -y install ca-certificates curl gnupg && \
sudo install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
sudo chmod a+r /etc/apt/keyrings/docker.gpg && \
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && \
sudo apt-get update && \
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

RUN dotnet dev-certs https
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace AzureIoTHub.Portal.Application.Providers
{
using System.Threading;
using System.Threading.Tasks;
using AzureIoTHub.Portal.Models.v10;
using Microsoft.Azure.Devices.Provisioning.Service;
using Microsoft.Azure.Devices.Shared;
using Shared.Models.v10;

public interface IDeviceRegistryProvider
{
Expand Down Expand Up @@ -38,7 +38,7 @@ public interface IDeviceRegistryProvider
/// </summary>
/// <param name="deviceId">The device identifier.</param>
/// <param name="modelId">The device model id.</param>
Task<EnrollmentCredentials> GetEnrollmentCredentialsAsync(string deviceId, string modelId);
Task<DeviceCredentials> GetEnrollmentCredentialsAsync(string deviceId, string modelId);

Task DeleteEnrollmentGroupAsync(EnrollmentGroup enrollmentGroup, CancellationToken cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ public interface IAWSExternalDeviceService
Task<GetThingShadowResponse> GetDeviceShadow(string deviceName);

Task<UpdateThingShadowResponse> UpdateDeviceShadow(UpdateThingShadowRequest shadow);


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Task<PaginatedResult<DeviceListItem>> GetDevices(

Task DeleteDevice(string deviceId);

Task<EnrollmentCredentials> GetCredentials(string deviceId);
Task<DeviceCredentials> GetCredentials(string deviceId);

Task<IEnumerable<LoRaDeviceTelemetryDto>> GetDeviceTelemetry(string deviceId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@ Task<PaginatedResult<IoTEdgeListItem>> GetEdgeDevicesPage(
Task<C2Dresult> ExecuteModuleCommand(string deviceId, string moduleName, string commandName);

Task<IEnumerable<LabelDto>> GetAvailableLabels();

Task<string> GetEdgeDeviceEnrollementScript(string deviceId, string templateName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace AzureIoTHub.Portal.Application.Services
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Shared;
using AzureIoTHub.Portal.Domain.Shared;
using Shared.Models.v10;

public interface IExternalDeviceService
{
Expand All @@ -21,10 +22,13 @@ public interface IExternalDeviceService
Task<Twin> GetDeviceTwin(string deviceId);

Task<Twin> GetDeviceTwinWithModule(string deviceId);

Task<Twin> GetDeviceTwinWithEdgeHubModule(string deviceId);

Task<BulkRegistryOperationResult> CreateDeviceWithTwin(string deviceId, bool isEdge, Twin twin, DeviceStatus isEnabled);

Task<bool> CreateEdgeDevice(string deviceId);

Task<Device> UpdateDevice(Device device);

Task<Twin> UpdateDeviceTwin(Twin twin);
Expand Down Expand Up @@ -64,9 +68,9 @@ Task<PaginationResult<Twin>> GetAllEdgeDevice(

Task<int> GetConcentratorsCount();

Task<EnrollmentCredentials> GetEnrollmentCredentials(string deviceId);
Task<DeviceCredentials> GetDeviceCredentials(string deviceId);

Task<EnrollmentCredentials> GetEdgeDeviceCredentials(string edgeDeviceId);
Task<DeviceCredentials> GetEdgeDeviceCredentials(IoTEdgeDevice device);

Task<ConfigItem> RetrieveLastConfiguration(Twin twin);

Expand All @@ -75,5 +79,9 @@ Task<PaginationResult<Twin>> GetAllEdgeDevice(
Task<List<string>> GetAllGatewayID();

Task<IEnumerable<string>> GetDevicesToExport();

Task<string> CreateEnrollementScript(string template, Domain.Entities.EdgeDevice device);

Task RemoveDeviceCredentials(IoTEdgeDevice device);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

[CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public string deviceId { get; set; } = default!;
private EnrollmentCredentials credentials = new();
private SymmetricCredentials credentials = new();

protected override async Task OnInitializedAsync()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,86 @@
@using AzureIoTHub.Portal.Models.v10
@using AzureIoTHub.Portal.Shared.Constants;

@inject ClipboardService ClipboardService
@inject IEdgeDeviceClientService EdgeDeviceClientService
@inject PortalSettings Portal

<MudDialog>
<DialogContent>
<MudCard Outlined="true">
<MudCardContent>
<MudGrid>
<DialogContent>
<MudCard Outlined="true">
<MudCardContent>
<MudGrid>
@if (Portal.CloudProvider.Equals(CloudProviders.Azure))
{
<MudItem xs="12">
<MudText Style="text-decoration:underline"><b>Service Endpoint</b></MudText>
<MudTextField @bind-Value="@Credentials.ProvisioningEndpoint" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.ProvisioningEndpoint))"/>
<MudTextField @bind-Value="@Credentials.ProvisioningEndpoint" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.ProvisioningEndpoint))" />
</MudItem>
<MudItem Class="mt-0" xs="12">
<MudText Style="text-decoration:underline"><b>Registration Id</b></MudText>
<MudTextField @bind-Value="@Credentials.RegistrationID" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.RegistrationID))" />
<MudTextField @bind-Value="@Credentials.RegistrationID" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.RegistrationID))" />
</MudItem>
<MudItem Class="mt-0" xs="12">
<MudText Style="text-decoration:underline"><b>Scope Id</b></MudText>
<MudTextField @bind-Value="@Credentials.ScopeID" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.ScopeID))" />
<MudTextField @bind-Value="@Credentials.ScopeID" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.ScopeID))" />
</MudItem>
<MudItem Class="mt-0" xs="12">
<MudText Style="text-decoration:underline"><b>Symmetric Key</b></MudText>
<MudTextField @bind-Value="@Credentials.SymmetricKey" Variant="Variant.Text" InputType="InputType.Password" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(Credentials.SymmetricKey))" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</DialogContent>
<DialogActions>
<MudButton id="cancel" OnClick="Cancel">Cancel</MudButton>
</DialogActions>
</MudDialog>
}
else if (Portal.CloudProvider.Equals(CloudProviders.AWS))
{
<MudItem xs="12">
@if (string.IsNullOrEmpty(EnrollementScriptCommand))
{
<MudText>Quick connect</MudText>
<br />
<MudAlert Severity="Severity.Normal">
Quiclky connect your Edge device on the platform by executing one command.
</MudAlert>
<br />
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Style="margin:0.5em;display: block; margin-left: auto; margin-right: auto" OnClick="GetEnrollmentScriptCommand">Get the magic command</MudButton>
}
else
{
<br />
<MudText Style="text-decoration:underline"><b>Operating system</b></MudText>
<MudSelect T="string" @bind-Value="@templateName" Text="Operating system" SelectedValuesChanged="GetEnrollmentScriptCommand">
<MudSelectItem Value="@("debian_11_bullseye")">Debian 11 (Bulleseye)</MudSelectItem>
</MudSelect>
<br />
<MudAlert Severity="Severity.Info">
Copy this command line above and paste it into the device prompt.<br />
<b>Note that you should have administrative rights on the device to execute the command.</b>
</MudAlert>
<br />
<MudTextField @bind-Value="@EnrollementScriptCommand" Style="text-overflow: ellipsis" Class="mt-0" Variant="Variant.Text" Margin="Margin.Dense" ReadOnly="true" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" OnAdornmentClick="@(() => ClipboardService.WriteTextAsync(EnrollementScriptCommand))" />
}
</MudItem>
}
</MudGrid>
</MudCardContent>
</MudCard>
</DialogContent>
<DialogActions>
<MudButton id="ok" OnClick="Close">OK</MudButton>
</DialogActions>
</MudDialog>

@code {
[CascadingParameter]
public Error Error { get; set; } = default!;

[CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!;

[Parameter] public string deviceId { get; set; } = default!;

private EnrollmentCredentials Credentials = new EnrollmentCredentials();
private SymmetricCredentials Credentials = new SymmetricCredentials();

public string EnrollementScriptCommand { get; set; } = default!;

public string templateName { get; set; } = "debian_11_bullseye";

protected override async Task OnInitializedAsync()
{
Expand All @@ -53,10 +92,16 @@
catch (ProblemDetailsException exception)
{
Error?.ProcessProblemDetails(exception);
Cancel();
Close();
}
}

private async Task GetEnrollmentScriptCommand()
{
var url = await EdgeDeviceClientService.GetEnrollmentScriptUrl(deviceId, templateName);

EnrollementScriptCommand = $"curl -s {url} | bash";
}

void Cancel() => MudDialog.Cancel();
void Close() => MudDialog.Cancel();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@attribute [Authorize]
@inject PortalSettings Portal
@inject ISnackbar Snackbar
@inject PortalSettings Portal
@inject NavigationManager NavigationManager
@inject IEdgeDeviceClientService EdgeDeviceClientService
@inject IEdgeModelClientService EdgeModelClientService
Expand All @@ -20,9 +21,10 @@
<MudGrid>
<MudItem xs="12" sm="4" md="3">
<MudCard>
<MudCardHeader>
<MudCardHeader Class="DeviceCardHeader">
<CardHeaderContent>
<MudText Typo="Typo.h5" Align="Align.Center">@(string.IsNullOrEmpty(EdgeDevice.DeviceName) ? EdgeDevice.DeviceId : EdgeDevice.DeviceName)</MudText>
<MudText Typo="Typo.body2" Class="mb-6" id=@nameof(IoTEdgeModel.Name)>Model: <b>@edgeModel?.Name</b></MudText>
<MudText Typo="Typo.h5" Class="overflow-ellipsis" Align="Align.Center">@(string.IsNullOrEmpty(EdgeDevice?.DeviceName) ? EdgeDevice?.DeviceId : EdgeDevice?.DeviceName)</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
Expand Down Expand Up @@ -90,12 +92,16 @@

</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="@EdgeDevice.DeviceId"
id=@nameof(IoTEdgeDevice.DeviceId)
Label="Device ID"
Variant="Variant.Outlined"
For="@(()=> EdgeDevice.DeviceId)"
HelperText="The device identifier should be of ASCII 7-bit alphanumeric characters plus certain special characters" />
@if (Portal.CloudProvider.Equals(CloudProviders.Azure))
{
<MudTextField @bind-Value="@EdgeDevice.DeviceId"
id=@nameof(IoTEdgeDevice.DeviceId)
Label="Device ID"
Variant="Variant.Outlined"
For="@(()=> EdgeDevice.DeviceId)"
HelperText="The device identifier should be of ASCII 7-bit alphanumeric characters plus certain special characters" />

}
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Checked="@duplicateDevice" Label="Or select a device to copy" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@
{
var parameters = new DialogParameters { { nameof(ConnectionStringDialog.deviceId), this.deviceId } };

_ = await DialogService.Show<ConnectionStringDialog>("Edge Device Connection String", parameters).Result;
_ = await DialogService.Show<ConnectionStringDialog>("Connect Edge device", parameters).Result;
}

public async Task ShowDeleteModal()
Expand Down
4 changes: 2 additions & 2 deletions src/AzureIoTHub.Portal.Client/Services/DeviceClientService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ public Task SetDeviceProperties(string deviceId, IList<DevicePropertyValue> devi
return this.http.PostAsJsonAsync($"api/devices/{deviceId}/properties", deviceProperties);
}

public Task<EnrollmentCredentials> GetEnrollmentCredentials(string deviceId)
public Task<SymmetricCredentials> GetEnrollmentCredentials(string deviceId)
{
return this.http.GetFromJsonAsync<EnrollmentCredentials>($"api/devices/{deviceId}/credentials")!;
return this.http.GetFromJsonAsync<SymmetricCredentials>($"api/devices/{deviceId}/credentials")!;
}

public Task DeleteDevice(string deviceId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ public Task DeleteDevice(string deviceId)
return this.http.DeleteAsync($"api/edge/devices/{deviceId}");
}

public Task<EnrollmentCredentials> GetEnrollmentCredentials(string deviceId)
public Task<SymmetricCredentials> GetEnrollmentCredentials(string deviceId)
{
return this.http.GetFromJsonAsync<EnrollmentCredentials>($"api/edge/devices/{deviceId}/credentials")!;
return this.http.GetFromJsonAsync<SymmetricCredentials>($"api/edge/devices/{deviceId}/credentials")!;
}

public Task<string> GetEnrollmentScriptUrl(string deviceId, string templateName)
{
return this.http.GetStringAsync($"api/edge/devices/{deviceId}/enrollementScript/{templateName}")!;
}

public async Task<List<IoTEdgeDeviceLog>> GetEdgeDeviceLogs(string deviceId, IoTEdgeModule edgeModule)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public interface IDeviceClientService

Task SetDeviceProperties(string deviceId, IList<DevicePropertyValue> deviceProperties);

Task<EnrollmentCredentials> GetEnrollmentCredentials(string deviceId);
Task<SymmetricCredentials> GetEnrollmentCredentials(string deviceId);

Task DeleteDevice(string deviceId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public interface IEdgeDeviceClientService

Task DeleteDevice(string deviceId);

Task<EnrollmentCredentials> GetEnrollmentCredentials(string deviceId);
Task<SymmetricCredentials> GetEnrollmentCredentials(string deviceId);

Task<string> GetEnrollmentScriptUrl(string deviceId, string templateName);

Task<List<IoTEdgeDeviceLog>> GetEdgeDeviceLogs(string deviceId, IoTEdgeModule edgeModule);

Expand Down
Loading

0 comments on commit 60ba3cf

Please sign in to comment.