diff --git a/azure-test/tests/azure_iothub/dependencies.txt b/azure-test/tests/azure_iothub/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/azure-test/tests/azure_iothub/test-get-expected.json b/azure-test/tests/azure_iothub/test-get-expected.json new file mode 100644 index 00000000..e4adb8b9 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-get-expected.json @@ -0,0 +1,10 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "region": "{{ output.location.value }}", + "resource_group": "{{ resourceName }}", + "subscription_id": "{{ output.subscription_id.value }}", + "type": "Microsoft.Devices/IotHubs" + } +] diff --git a/azure-test/tests/azure_iothub/test-get-query.sql b/azure-test/tests/azure_iothub/test-get-query.sql new file mode 100644 index 00000000..e2045b51 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-get-query.sql @@ -0,0 +1,3 @@ +select name, id, type, region, resource_group, subscription_id +from azure.azure_iothub +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; \ No newline at end of file diff --git a/azure-test/tests/azure_iothub/test-list-expected.json b/azure-test/tests/azure_iothub/test-list-expected.json new file mode 100644 index 00000000..6175f01d --- /dev/null +++ b/azure-test/tests/azure_iothub/test-list-expected.json @@ -0,0 +1,7 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "type": "Microsoft.Devices/IotHubs" + } +] diff --git a/azure-test/tests/azure_iothub/test-list-query.sql b/azure-test/tests/azure_iothub/test-list-query.sql new file mode 100644 index 00000000..9180d542 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-list-query.sql @@ -0,0 +1,3 @@ +select name, id, type +from azure.azure_iothub +where name = '{{ resourceName }}'; \ No newline at end of file diff --git a/azure-test/tests/azure_iothub/test-not-found-expected.json b/azure-test/tests/azure_iothub/test-not-found-expected.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-not-found-expected.json @@ -0,0 +1 @@ +null diff --git a/azure-test/tests/azure_iothub/test-not-found-query.sql b/azure-test/tests/azure_iothub/test-not-found-query.sql new file mode 100644 index 00000000..9c8bc964 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, tags, title, akas +from azure.azure_iothub +where name = 'dummy-{{ resourceName }}' and resource_group = '{{ resourceName }}'; \ No newline at end of file diff --git a/azure-test/tests/azure_iothub/test-turbot-expected.json b/azure-test/tests/azure_iothub/test-turbot-expected.json new file mode 100644 index 00000000..02cd3c76 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-turbot-expected.json @@ -0,0 +1,10 @@ +[ + { + "akas": [ + "{{ output.resource_aka.value }}", + "{{ output.resource_aka_lower.value }}" + ], + "name": "{{ resourceName }}", + "title": "{{ resourceName }}" + } +] diff --git a/azure-test/tests/azure_iothub/test-turbot-query.sql b/azure-test/tests/azure_iothub/test-turbot-query.sql new file mode 100644 index 00000000..13863d62 --- /dev/null +++ b/azure-test/tests/azure_iothub/test-turbot-query.sql @@ -0,0 +1,3 @@ +select name, title, akas +from azure.azure_iothub +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; \ No newline at end of file diff --git a/azure-test/tests/azure_iothub/variables.json b/azure-test/tests/azure_iothub/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/azure-test/tests/azure_iothub/variables.json @@ -0,0 +1 @@ +{} diff --git a/azure-test/tests/azure_iothub/variables.tf b/azure-test/tests/azure_iothub/variables.tf new file mode 100644 index 00000000..6420c515 --- /dev/null +++ b/azure-test/tests/azure_iothub/variables.tf @@ -0,0 +1,143 @@ +variable "resource_name" { + type = string + default = "steampipe-test" + description = "Name of the resource used throughout the test." +} + +variable "azure_environment" { + type = string + default = "public" + description = "Azure environment used for the test." +} + +variable "azure_subscription" { + type = string + default = "3510ae4d-530b-497d-8f30-53c0616fc6c1" + description = "Azure subscription used for the test." +} + +provider "azurerm" { + # Cannot be passed as a variable + version = "=1.36.0" + environment = var.azure_environment + subscription_id = var.azure_subscription +} + +data "azurerm_client_config" "current" {} + +data "null_data_source" "resource" { + inputs = { + scope = "azure:///subscriptions/${data.azurerm_client_config.current.subscription_id}" + } +} + +resource "azurerm_resource_group" "named_test_resource" { + name = var.resource_name + location = "East US" +} + +resource "azurerm_storage_account" "named_test_resource" { + name = var.resource_name + resource_group_name = azurerm_resource_group.named_test_resource.name + location = azurerm_resource_group.named_test_resource.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "named_test_resource" { + name = var.resource_name + storage_account_name = azurerm_storage_account.named_test_resource.name + container_access_type = "private" +} + +resource "azurerm_eventhub_namespace" "named_test_resource" { + name = var.resource_name + resource_group_name = azurerm_resource_group.named_test_resource.name + location = azurerm_resource_group.named_test_resource.location + sku = "Basic" +} + +resource "azurerm_eventhub" "named_test_resource" { + name = var.resource_name + resource_group_name = azurerm_resource_group.named_test_resource.name + namespace_name = azurerm_eventhub_namespace.named_test_resource.name + partition_count = 2 + message_retention = 1 +} + +resource "azurerm_eventhub_authorization_rule" "named_test_resource" { + resource_group_name = azurerm_resource_group.named_test_resource.name + namespace_name = azurerm_eventhub_namespace.named_test_resource.name + eventhub_name = azurerm_eventhub.named_test_resource.name + name = var.resource_name + send = true +} + +resource "azurerm_iothub" "named_test_resource" { + name = var.resource_name + resource_group_name = azurerm_resource_group.named_test_resource.name + location = azurerm_resource_group.named_test_resource.location + + sku { + name = "S1" + capacity = "1" + tier = "Basic" + } + + endpoint { + type = "AzureIotHub.StorageContainer" + connection_string = azurerm_storage_account.named_test_resource.primary_blob_connection_string + name = "export" + batch_frequency_in_seconds = 60 + max_chunk_size_in_bytes = 10485760 + container_name = azurerm_storage_container.named_test_resource.name + encoding = "Avro" + file_name_format = "{iothub}/{partition}_{YYYY}_{MM}_{DD}_{HH}_{mm}" + } + + endpoint { + type = "AzureIotHub.EventHub" + connection_string = azurerm_eventhub_authorization_rule.named_test_resource.primary_connection_string + name = "export2" + } + + route { + name = "export" + source = "DeviceMessages" + condition = "true" + endpoint_names = ["export"] + enabled = true + } + + route { + name = "export2" + source = "DeviceMessages" + condition = "true" + endpoint_names = ["export2"] + enabled = true + } +} + +output "resource_aka" { + value = "azure://${azurerm_iothub.named_test_resource.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_iothub.named_test_resource.id)}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = azurerm_iothub.named_test_resource.id +} + +output "location" { + value = azurerm_resource_group.named_test_resource.location +} + +output "subscription_id" { + value = var.azure_subscription +} diff --git a/azure/plugin.go b/azure/plugin.go index 00799d95..84e13697 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -58,6 +58,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_eventhub_namespace": tableAzureEventHubNamespace(ctx), "azure_express_route_circuit": tableAzureExpressRouteCircuit(ctx), "azure_firewall": tableAzureFirewall(ctx), + "azure_iothub": tableAzureIotHub(ctx), "azure_key_vault": tableAzureKeyVault(ctx), "azure_key_vault_key": tableAzureKeyVaultKey(ctx), "azure_key_vault_secret": tableAzureKeyVaultSecret(ctx), diff --git a/azure/table_azure_iothub.go b/azure/table_azure_iothub.go new file mode 100644 index 00000000..106578aa --- /dev/null +++ b/azure/table_azure_iothub.go @@ -0,0 +1,317 @@ +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/monitor/mgmt/insights" + "github.com/Azure/azure-sdk-for-go/services/iothub/mgmt/2020-03-01/devices" + "github.com/turbot/steampipe-plugin-sdk/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/plugin/transform" + + "github.com/turbot/steampipe-plugin-sdk/plugin" +) + +//// TABLE DEFINITION + +func tableAzureIotHub(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_iothub", + Description: "Azure Iot Hub", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"name", "resource_group"}), + Hydrate: getIotHub, + ShouldIgnoreError: isNotFoundError([]string{"ResourceNotFound", "ResourceGroupNotFound", "400"}), + }, + List: &plugin.ListConfig{ + Hydrate: listIotHubs, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Description: "The resource name.", + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The resource identifier.", + Type: proto.ColumnType_STRING, + Transform: transform.FromGo(), + }, + { + Name: "state", + Description: "The iot hub state.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.State"), + }, + { + Name: "provisioning_state", + Description: "The iot hub provisioning state.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.ProvisioningState"), + }, + { + Name: "type", + Description: "The resource type.", + Type: proto.ColumnType_STRING, + }, + { + Name: "comments", + Description: "Iot hub comments.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.Comments"), + }, + { + Name: "enable_file_upload_notifications", + Description: "Indicates if file upload notifications are enabled for the iot hub.", + Type: proto.ColumnType_BOOL, + Transform: transform.FromField("Properties.EnableFileUploadNotifications"), + }, + { + Name: "etag", + Description: "An unique read-only string that changes whenever the resource is updated.", + Type: proto.ColumnType_STRING, + }, + { + Name: "features", + Description: "The capabilities and features enabled for the iot hub. Possible values include: 'None', 'DeviceManagement'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.Features"), + }, + { + Name: "host_name", + Description: "The name of the host.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.HostName"), + }, + { + Name: "min_tls_version", + Description: "Specifies the minimum TLS version to support for this iot hub.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.MinTLSVersion"), + }, + { + Name: "public_network_access", + Description: "Indicates whether requests from public network are allowed.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.PublicNetworkAccess").Transform(transform.ToString), + }, + { + Name: "sku_capacity", + Description: "Iot hub SKU capacity.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Sku.Capacity"), + }, + { + Name: "sku_name", + Description: "Iot hub SKU name.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Sku.Name").Transform(transform.ToString), + }, + { + Name: "sku_tier", + Description: "Iot hub SKU tier.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Sku.Tier"), + }, + { + Name: "authorization_policies", + Description: "The shared access policies you can use to secure a connection to the iot hub.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.AuthorizationPolicies"), + }, + { + Name: "cloud_to_device", + Description: "CloudToDevice properties of the iot hub.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.CloudToDevice"), + }, + { + Name: "diagnostic_settings", + Description: "A list of active diagnostic settings for the iot hub.", + Type: proto.ColumnType_JSON, + Hydrate: listIotHubDiagnosticSettings, + Transform: transform.FromValue(), + }, + { + Name: "event_hub_endpoints", + Description: "The event hub-compatible endpoint properties.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.EventHubEndpoints"), + }, + { + Name: "ip_filter_rules", + Description: "The IP filter rules of the iot hub.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.IPFilterRules"), + }, + { + Name: "locations", + Description: "Primary and secondary location for iot hub.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.Locations"), + }, + { + Name: "messaging_endpoints", + Description: "The messaging endpoint properties for the file upload notification queue.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.MessagingEndpoints"), + }, + { + Name: "private_endpoint_connections", + Description: "Private endpoint connections created on this iot hub.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.PrivateEndpointConnections"), + }, + { + Name: "routing", + Description: "Routing properties of the iot hub.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.Routing"), + }, + { + Name: "storage_endpoints", + Description: "The list of azure storage endpoints where you can upload files.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.StorageEndpoints"), + }, + + // Steampipe standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "tags", + Description: ColumnDescriptionTags, + Type: proto.ColumnType_JSON, + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ID").Transform(idToAkas), + }, + + // Azure standard column + { + Name: "region", + Description: ColumnDescriptionRegion, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Location").Transform(toLower), + }, + { + Name: "resource_group", + Description: ColumnDescriptionResourceGroup, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ID").Transform(extractResourceGroupFromID), + }, + { + Name: "subscription_id", + Description: ColumnDescriptionSubscription, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ID").Transform(idToSubscriptionID), + }, + }, + } +} + +//// LIST FUNCTION + +func listIotHubs(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + iotHubClient := devices.NewIotHubResourceClient(subscriptionID) + iotHubClient.Authorizer = session.Authorizer + + pagesLeft := true + for pagesLeft { + result, err := iotHubClient.ListBySubscription(context.Background()) + if err != nil { + return nil, err + } + for _, iotHubDescription := range result.Values() { + d.StreamListItem(ctx, iotHubDescription) + } + result.NextWithContext(context.Background()) + pagesLeft = result.NotDone() + } + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getIotHub(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("getIotHub") + + // Create session + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + iotHubClient := devices.NewIotHubResourceClient(subscriptionID) + iotHubClient.Authorizer = session.Authorizer + + name := d.KeyColumnQuals["name"].GetStringValue() + resourceGroup := d.KeyColumnQuals["resource_group"].GetStringValue() + + // Return nil, if no input provide + if name == "" || resourceGroup == "" { + return nil, nil + } + + op, err := iotHubClient.Get(ctx, resourceGroup, name) + if err != nil { + return nil, err + } + + return op, nil +} + +func listIotHubDiagnosticSettings(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listIotHubDiagnosticSettings") + id := *h.Item.(devices.IotHubDescription).ID + + // Create session + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + client := insights.NewDiagnosticSettingsClient(subscriptionID) + client.Authorizer = session.Authorizer + + op, err := client.List(ctx, id) + if err != nil { + return nil, err + } + + // If we return the API response directly, the output only gives + // the contents of DiagnosticSettings + var diagnosticSettings []map[string]interface{} + for _, i := range *op.Value { + objectMap := make(map[string]interface{}) + if i.ID != nil { + objectMap["id"] = i.ID + } + if i.Name != nil { + objectMap["name"] = i.Name + } + if i.Type != nil { + objectMap["type"] = i.Type + } + if i.DiagnosticSettings != nil { + objectMap["properties"] = i.DiagnosticSettings + } + diagnosticSettings = append(diagnosticSettings, objectMap) + } + return diagnosticSettings, nil +} diff --git a/docs/tables/azure_iothub.md b/docs/tables/azure_iothub.md new file mode 100644 index 00000000..bc0f7b4e --- /dev/null +++ b/docs/tables/azure_iothub.md @@ -0,0 +1,31 @@ +# Table: azure_iothub + +Azure IoT Hub is Microsoft’s Internet of Things connector to the cloud. It’s a fully managed cloud service that enables reliable and secure bi-directional communications between millions of IoT devices and a solution back end. + +## Examples + +### Basic info + +```sql +select + name, + id, + region, + type +from + azure_iothub; +``` + +### List hubs which are not active + +```sql +select + name, + id, + region, + type +from + azure_iothub +where + state <> 'Active'; +```