Importación y exportación de identidades de dispositivo de IoT Hub de forma masiva

Cada centro de IoT tiene un registro de identidades que se puede usar para crear recursos por dispositivo en el servicio. El registro de identidad también permite controlar el acceso a los puntos de conexión accesibles desde los dispositivos. En este artículo, se describe cómo importar y exportar identidades de dispositivo de forma masiva hacia un registro de identidad y desde este, mediante el ejemplo ImportExportDeviceSample incluido con el SDK de Microsoft Azure IoT para .NET. Para obtener información sobre cómo se puede usar esta funcionalidad al migrar un centro de IoT a otra región, consulte Cómo migrar manualmente un centro de Azure IoT mediante plantillas de Azure Resource Manager.

Nota:

El centro de IoT agregó recientemente compatibilidad con redes virtuales en un número limitado de regiones. Esta característica protege las operaciones de importación y exportación y elimina la necesidad de pasar claves para la autenticación. Actualmente, la compatibilidad con redes virtuales solo está disponible en estas regiones: Oeste de EE. UU. 2, Este de EE. UU. y Centro-sur de EE. UU.. Para más información sobre la compatibilidad con redes virtuales y las llamadas API para implementarla, consulte Compatibilidad de IoT Hub con redes virtuales.

Las operaciones de importación y exportación tienen lugar en el contexto de trabajos que permiten ejecutar operaciones de servicio de forma masiva en centros de IoT.

La clase RegistryManager en el SDK incluye los métodos ExportDevicesAsync y ImportDevicesAsync que usan el marco Trabajo. Estos métodos le permiten exportar, importar y sincronizar la totalidad de un Registro de identidad de IoT Hub.

En este artículo se describe el uso de la clase RegistryManager y del sistema de trabajo para realizar importaciones y exportaciones en bloque de dispositivos a un registro de identidades de un centro de IoT y desde este. También es posible usar el servicio Azure IoT Hub Device Provisioning para habilitar el aprovisionamiento Just-In-Time sin intervención del usuario de uno o varios centros de IoT. Para más información, consulte la documentación del servicio de aprovisionamiento.

Nota:

Algunos de los fragmentos de código de este artículo se incluyen en el ejemplo de servicio ImportExportDevicesSample proporcionado con el SDK de Microsoft Azure IoT para .NET. El ejemplo se encuentra en la carpeta /iothub/service/samples/how to guides/ImportExportDevicesSample del SDK y, donde se especifican, se incluyen fragmentos de código del archivo ImportExportDevicesSample.cs para ese ejemplo de SDK. Para obtener más información sobre el ejemplo ImportExportDevicesSample y otros ejemplos de servicio incluidos en el SDK de Azure IoT para .NET, consulte Ejemplos de servicio de Azure IoT Hub para C#.

¿Qué son los trabajos?

Las operaciones de Registro de identidad usan el sistema de trabajo cuando la operación:

  • Tiene un tiempo de ejecución potencialmente largo en comparación con las operaciones en tiempo de ejecución estándar.

  • Devuelve una gran cantidad de datos al usuario.

En lugar de una única llamada API en espera o bloqueando el resultado de la operación, esta crea de forma asincrónica un trabajo para ese centro de IoT. La operación después devuelve inmediatamente un objeto JobProperties.

El siguiente fragmento de código de C# muestra cómo crear un trabajo de exportación:

// Call an export job on the IoT hub to retrieve all devices
JobProperties exportJob = await 
  registryManager.ExportDevicesAsync(containerSasUri, false);

Nota

Para usar la clase RegistryManager en el código de C#, agregue el paquete de NuGet Microsoft.Azure.Devices al proyecto. La clase RegistryManager está en el espacio de nombres Microsoft.Azure.Devices.

Puede usar la clase RegistryManager para consultar el estado de Trabajo con los metadatos de JobProperties devueltos. Para crear una instancia de la clase RegistryManager, use el método CreateFromConnectionString.

RegistryManager registryManager =
  RegistryManager.CreateFromConnectionString("{your IoT Hub connection string}");

Para buscar la cadena de conexión de su IoT Hub en Azure Portal:

  1. Vaya a su instancia de IoT Hub.

  2. Seleccione Directivas de acceso compartido.

  3. Seleccione una directiva teniendo en cuenta los permisos que necesita.

  4. Copie la cadena de conexión de esa directiva.

El siguiente fragmento de código de C#, del método WaitForJobAsync en el SDK de ejemplo, muestra cómo sondear cada cinco segundos para ver si el trabajo ha terminado de ejecutarse:

// Wait until job is finished
while (true)
{
    job = await registryManager.GetJobAsync(job.JobId);
    if (job.Status == JobStatus.Completed
        || job.Status == JobStatus.Failed
        || job.Status == JobStatus.Cancelled)
    {
        // Job has finished executing
        break;
    }
    Console.WriteLine($"\tJob status is {job.Status}...");

    await Task.Delay(TimeSpan.FromSeconds(5));
}

Nota

Si la cuenta de almacenamiento tiene configuraciones de firewall que restringen la conectividad de IoT Hub, considere la posibilidad de usar excepciones propias de confianza de Microsoft (disponibles en las regiones seleccionadas para centros de IoT con identidad de servicio administrada).

Límites de trabajo de importación y exportación de dispositivos

Solo se permite un trabajo activo de importación o exportación de dispositivos de forma simultánea para todos los niveles de IoT Hub. IoT Hub también tiene límites de velocidad para las operaciones de trabajos. Para obtener más información, consulte Cuotas y límites de IoT Hub.

Exportación de dispositivos

Use el método ExportDevicesAsync para exportar la totalidad de un registro de identidades de IoT Hub a un contenedor de blobs de Azure Storage mediante una firma de acceso compartido (SAS). Este método le permite crear copias de seguridad confiables de la información del dispositivo en un contenedor de blobs que usted controle.

El método ExportDevicesAsync requiere dos parámetros:

  • Una cadena que contiene un identificador URI de un contenedor de blobs. Este identificador URI debe contener un token SAS que conceda acceso de escritura al contenedor. El trabajo crea un blob en bloques en este contenedor para almacenar los datos serializados de exportación del dispositivo. El token de SAS debe incluir estos permisos:

    SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.Read 
       | SharedAccessBlobPermissions.Delete
    
  • Un valor booleano que indica si desea excluir las claves de autenticación de los datos de exportación. Si es false, las claves de autenticación se incluyen en la exportación. De lo contrario, las claves se exportan como null.

El siguiente fragmento de código de C# muestra cómo iniciar un trabajo de exportación que incluye las claves de autenticación de dispositivo en los datos de exportación y luego sondea la finalización:

// Call an export job on the IoT Hub to retrieve all devices
JobProperties exportJob = 
  await registryManager.ExportDevicesAsync(containerSasUri, false);

// Wait until job is finished
while(true)
{
    exportJob = await registryManager.GetJobAsync(exportJob.JobId);
    if (exportJob.Status == JobStatus.Completed || 
        exportJob.Status == JobStatus.Failed ||
        exportJob.Status == JobStatus.Cancelled)
    {
    // Job has finished executing
    break;
    }

    await Task.Delay(TimeSpan.FromSeconds(5));
}

Puede encontrar código similar en el método ExportDevicesAsync del SDK de ejemplo. El trabajo almacena su salida en el contenedor de blobs proporcionado como un blob en bloques con el nombre devices.txt. Los datos de salida se componen de datos de dispositivos serializados de JSON, con un dispositivo por línea.

En el siguiente ejemplo se muestran los datos de salida:

{"id":"Device1","eTag":"MA==","status":"enabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device2","eTag":"MA==","status":"enabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device3","eTag":"MA==","status":"disabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device4","eTag":"MA==","status":"disabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device5","eTag":"MA==","status":"enabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}

Si un dispositivo tiene datos gemelos, estos también se exportan junto con los datos del dispositivo. En el siguiente ejemplo se muestra este formato. Todos los datos de la línea "twinETag" hasta el final son datos gemelos.

{
   "id":"export-6d84f075-0",
   "eTag":"MQ==",
   "status":"enabled",
   "authentication":null,
   "twinETag":"AAAAAAAAAAI=",
   "tags":{
      "Location":"LivingRoom"
   },
   "properties":{
      "desired":{
         "Thermostat":{
            "Temperature":75.1,
            "Unit":"F"
         },
      },
      "reported":{}
   }
}

Si necesitase acceso a estos datos en el código, deserialice estos datos mediante la clase ExportImportDevice. El siguiente fragmento de código de C#, del método ReadFromBlobAsync en el SDK de ejemplo, muestra cómo leer la información del dispositivo que se exportó anteriormente desde ExportImportDevice a una instancia de BlobClient:

private static async Task<List<string>> ReadFromBlobAsync(BlobClient blobClient)
{
    // Read the blob file of devices, import each row into a list.
    var contents = new List<string>();

    using Stream blobStream = await blobClient.OpenReadAsync();
    using var streamReader = new StreamReader(blobStream, Encoding.UTF8);
    while (streamReader.Peek() != -1)
    {
        string line = await streamReader.ReadLineAsync();
        contents.Add(line);
    }

    return contents;
}

Importación de dispositivos

El método ImportDevicesAsync de la clase RegistryManager le permite realizar operaciones de sincronización e importación masiva en un Registro de identidad de IoT Hub. Al igual que el método ExportDevicesAsync, el método ImportDevicesAsync usa el marco Trabajo.

Tenga cuidado con el método ImportDevicesAsync porque además del aprovisionamiento de nuevos dispositivos en el Registro de identidad, también puede actualizar y eliminar dispositivos existentes.

Advertencia

Una operación de importación no se puede deshacer. Realice siempre una copia de seguridad de los datos existentes mediante el método ExportDevicesAsync en otro contenedor de blobs antes de realizar cambios masivos en el Registro de identidad.

El método ImportDevicesAsync requiere dos parámetros:

  • Una cadena que contenga un identificador URI de un contenedor de blobs de Azure Storage para usar como entrada para el trabajo. Este identificador URI debe contener un token SAS que conceda acceso de lectura al contenedor. Este contenedor debe incluir un blob con el nombre devices.txt que contenga los datos de dispositivo serializados para importar en el Registro de identidad. Los datos de importación deben contener la información del dispositivo en el mismo formato JSON que usa el trabajo ExportImportDevice cuando crea un blob devices.txt. El token de SAS debe incluir estos permisos:

    SharedAccessBlobPermissions.Read
    
  • Una cadena que contenga un identificador URI de un contenedor de blobs de Azure Storage para usar como salida del trabajo. El trabajo crea un blob en bloques en este contenedor para almacenar cualquier información de error del trabajo de importación finalizado. El token de SAS debe incluir estos permisos:

    SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.Read 
       | SharedAccessBlobPermissions.Delete
    

Nota

Los dos parámetros pueden apuntar al mismo contenedor de blobs. Los parámetros independientes simplemente habilitan más control sobre sus datos, ya que el contenedor de salida requiere permisos adicionales.

El siguiente fragmento de código de C# muestra cómo iniciar un trabajo de importación:

JobProperties importJob = 
   await registryManager.ImportDevicesAsync(containerSasUri, containerSasUri);

Este método también se puede usar para importar los datos para el dispositivo gemelo. El formato de la entrada de datos es el mismo que se muestra en la sección ExportDevicesAsync. De esta manera, se pueden volver a importar los datos exportados.

Comportamiento de la importación

Puede usar el método ImportDevicesAsync para realizar las siguientes operaciones de forma masiva en el Registro de identidad:

  • Registro masivo de nuevos dispositivos
  • Eliminación masiva de dispositivos existentes
  • Cambios masivos de estado (habilitar o deshabilitar dispositivos)
  • Asignación masiva de nuevas claves de autenticación de dispositivos
  • Regeneración automática masiva de claves de autenticación de dispositivos
  • Actualización masiva de datos gemelos

Puede realizar cualquier combinación de las operaciones precedentes en una única llamada ImportDevicesAsync. Por ejemplo, puede registrar nuevos dispositivos y eliminar o actualizar los dispositivos existentes al mismo tiempo. Cuando se utiliza junto con el método ExportDevicesAsync , puede migrar completamente todos los dispositivos de un Centro de IoT a otro.

Use la propiedad opcional importMode en los datos de serialización de importación para cada dispositivo para controlar el proceso de importación por dispositivo. La propiedad importMode tiene las siguientes opciones:

  • Crear
  • CreateOrUpdate (valor predeterminado)
  • CreateOrUpdateIfMatchETag
  • Eliminar
  • DeleteIfMatchETag
  • Actualizar
  • UpdateIfMatchETag
  • UpdateTwin
  • UpdateTwinIfMatchETag

Para obtener más información sobre cada una de estas opciones de modo de importación, consulte ImportMode

Solucionar problemas de trabajos de importación

El uso de un trabajo de importación para crear dispositivos podría producir un error con un problema de cuota cuando esté cerca del límite de recuento de dispositivos del centro de IoT. Este error puede ocurrir incluso si el recuento total de dispositivos sigue siendo inferior al límite de cuota. El error IotHubQuotaExceeded (403002) se devuelve con el siguiente mensaje de error: "El número total de dispositivos en IotHub superó la cuota asignada".

Si recibe este error, puede usar la siguiente consulta para devolver el número total de dispositivos registrados en IoT Hub:

SELECT COUNT() as totalNumberOfDevices FROM devices

Para obtener información sobre el número total de dispositivos que se pueden registrar en un centro de IoT, consulte límites de IoT Hub.

Si todavía hay cuota disponible, puede examinar el blob de salida del trabajo para los dispositivos que produjeron el error IotHubQuotaExceeded (403002). Después, puede intentar agregar cada uno de estos dispositivos al centro de IoT. Por ejemplo, puede usar los métodos AddDeviceAsync o AddDeviceWithTwinAsync. No intente agregar los dispositivos con otro trabajo, ya que se podría producir el mismo error.

Ejemplo de importación de dispositivos: aprovisionamiento masivo de dispositivos

El siguiente fragmento de código de C#, del método GenerateDevicesAsync en el SDK de ejemplo, muestra cómo generar varias identidades de dispositivo que:

  • Incluyen claves de autenticación.
  • Escriben la información del dispositivo en un blob en bloques.
  • Importan los dispositivos en el Registro de identidad.
private async Task GenerateDevicesAsync(RegistryManager registryManager, int numToAdd)
{
    var stopwatch = Stopwatch.StartNew();

    Console.WriteLine($"Creating {numToAdd} devices for the source IoT hub.");
    int interimProgressCount = 0;
    int displayProgressCount = 1000;
    int totalProgressCount = 0;

    // generate reference for list of new devices we're going to add, will write list to this blob
    BlobClient generateDevicesBlob = _blobContainerClient.GetBlobClient(_generateDevicesBlobName);

    // define serializedDevices as a generic list<string>
    var serializedDevices = new List<string>(numToAdd);

    for (int i = 1; i <= numToAdd; i++)
    {
        // Create device name with this format: Hub_00000000 + a new guid.
        // This should be large enough to display the largest number (1 million).
        string deviceName = $"Hub_{i:D8}_{Guid.NewGuid()}";
        Debug.Print($"Adding device '{deviceName}'");

        // Create a new ExportImportDevice.
        var deviceToAdd = new ExportImportDevice
        {
            Id = deviceName,
            Status = DeviceStatus.Enabled,
            Authentication = new AuthenticationMechanism
            {
                SymmetricKey = new SymmetricKey
                {
                    PrimaryKey = GenerateKey(32),
                    SecondaryKey = GenerateKey(32),
                }
            },
            // This indicates that the entry should be added as a new device.
            ImportMode = ImportMode.Create,
        };

        // Add device to the list as a serialized object.
        serializedDevices.Add(JsonConvert.SerializeObject(deviceToAdd));

        // Not real progress as you write the new devices, but will at least show *some* progress.
        interimProgressCount++;
        totalProgressCount++;
        if (interimProgressCount >= displayProgressCount)
        {
            Console.WriteLine($"Added {totalProgressCount}/{numToAdd} devices.");
            interimProgressCount = 0;
        }
    }

    // Now have a list of devices to be added, each one has been serialized.
    // Write the list to the blob.
    var sb = new StringBuilder();
    serializedDevices.ForEach(serializedDevice => sb.AppendLine(serializedDevice));

    // Write list of serialized objects to the blob.
    using Stream stream = await generateDevicesBlob.OpenWriteAsync(overwrite: true);
    byte[] bytes = Encoding.UTF8.GetBytes(sb.ToString());
    for (int i = 0; i < bytes.Length; i += BlobWriteBytes)
    {
        int length = Math.Min(bytes.Length - i, BlobWriteBytes);
        await stream.WriteAsync(bytes.AsMemory(i, length));
    }
    await stream.FlushAsync();

    Console.WriteLine("Running a registry manager job to add the devices.");

    // Should now have a file with all the new devices in it as serialized objects in blob storage.
    // generatedListBlob has the list of devices to be added as serialized objects.
    // Call import using the blob to add the new devices.
    // Log information related to the job is written to the same container.
    // This normally takes 1 minute per 100 devices (according to the docs).

    // First, initiate an import job.
    // This reads in the rows from the text file and writes them to IoT Devices.
    // If you want to add devices from a file, you can create a file and use this to import it.
    //   They have to be in the exact right format.
    try
    {
        // The first URI is the container to import from; the file defaults to devices.txt, but may be specified.
        // The second URI points to the container to write errors to as a blob.
        // This lets you import the devices from any file name. Since we wrote the new
        // devices to [devicesToAdd], need to read the list from there as well.
        var importGeneratedDevicesJob = JobProperties.CreateForImportJob(
            _containerUri,
            _containerUri,
            _generateDevicesBlobName);
        importGeneratedDevicesJob = await registryManager.ImportDevicesAsync(importGeneratedDevicesJob);
        await WaitForJobAsync(registryManager, importGeneratedDevicesJob);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Adding devices failed due to {ex.Message}");
    }

    stopwatch.Stop();
    Console.WriteLine($"GenerateDevices, time elapsed = {stopwatch.Elapsed}.");
}

Ejemplo de importación de dispositivos: eliminación masiva

El siguiente fragmento de código de C#, del método DeleteFromHubAsync en el SDK de ejemplo, muestra cómo eliminar todos los dispositivos de un centro de IoT:

private async Task DeleteFromHubAsync(RegistryManager registryManager, bool includeConfigurations)
{
    var stopwatch = Stopwatch.StartNew();

    Console.WriteLine("Deleting all devices from an IoT hub.");

    Console.WriteLine("Exporting a list of devices from IoT hub to blob storage.");

    // Read from storage, which contains serialized objects.
    // Write each line to the serializedDevices list.
    BlobClient devicesBlobClient = _blobContainerClient.GetBlobClient(_destHubDevicesImportBlobName);

    Console.WriteLine("Reading the list of devices in from blob storage.");
    List<string> serializedDevices = await ReadFromBlobAsync(devicesBlobClient);

    // Step 1: Update each device's ImportMode to be Delete
    Console.WriteLine("Updating ImportMode to be 'Delete' for each device and writing back to the blob.");
    var sb = new StringBuilder();
    serializedDevices.ForEach(serializedEntity =>
    {
        // Deserialize back to an ExportImportDevice and change import mode.
        ExportImportDevice device = JsonConvert.DeserializeObject<ExportImportDevice>(serializedEntity);
        device.ImportMode = ImportMode.Delete;

        // Reserialize the object now that we've updated the property.
        sb.AppendLine(JsonConvert.SerializeObject(device));
    });

    // Step 2: Write the list in memory to the blob.
    BlobClient deleteDevicesBlobClient = _blobContainerClient.GetBlobClient(_hubDevicesCleanupBlobName);
    await WriteToBlobAsync(deleteDevicesBlobClient, sb.ToString());

    // Step 3: Call import using the same blob to delete all devices.
    Console.WriteLine("Running a registry manager job to delete the devices from the IoT hub.");
    var importJob = JobProperties.CreateForImportJob(
        _containerUri,
        _containerUri,
        _hubDevicesCleanupBlobName);
    importJob = await registryManager.ImportDevicesAsync(importJob);
    await WaitForJobAsync(registryManager, importJob);

    // Step 4: delete configurations
    if (includeConfigurations)
    {
        BlobClient configsBlobClient = _blobContainerClient.GetBlobClient(_srcHubConfigsExportBlobName);
        List<string> serializedConfigs = await ReadFromBlobAsync(configsBlobClient);
        foreach (string serializedConfig in serializedConfigs)
        {
            try
            {
                Configuration config = JsonConvert.DeserializeObject<Configuration>(serializedConfig);
                await registryManager.RemoveConfigurationAsync(config.Id);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to deserialize or remove a config.\n\t{serializedConfig}\n\n{ex.Message}");
            }
        }
    }

    stopwatch.Stop();
    Console.WriteLine($"Deleted IoT hub devices and configs: time elapsed = {stopwatch.Elapsed}");
}

Obtención del identificador URI de SAS del contenedor

El ejemplo de código siguiente muestra cómo generar un identificador URI de SAS con permisos de lectura, escritura y eliminación para un contenedor de blobs:

static string GetContainerSasUri(CloudBlobContainer container)
{
  // Set the expiry time and permissions for the container.
  // In this case no start time is specified, so the
  // shared access signature becomes valid immediately.
  var sasConstraints = new SharedAccessBlobPolicy();
  sasConstraints.SharedAccessExpiryTime = DateTime.UtcNow.AddHours(24);
  sasConstraints.Permissions = 
    SharedAccessBlobPermissions.Write | 
    SharedAccessBlobPermissions.Read | 
    SharedAccessBlobPermissions.Delete;

  // Generate the shared access signature on the container,
  // setting the constraints directly on the signature.
  string sasContainerToken = container.GetSharedAccessSignature(sasConstraints);

  // Return the URI string for the container,
  // including the SAS token.
  return container.Uri + sasContainerToken;
}

Pasos siguientes

En este artículo, aprendió a realizar operaciones de forma masiva en el Registro de identidad en un centro de IoT. Muchas de estas operaciones, entre ellas cómo mover dispositivos de un centro a otro, se usan en la sección Administrar dispositivos registrados en el centro de IoT de Cómo migrar manualmente un centro de Azure IoT mediante plantillas de Azure Resource Manager.