Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Operational Insight Workspace (a.k.a Log Analytics) #331

Merged
merged 28 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
78cca50
Adding operationalinsight workspace
TsuyoshiUshio Sep 13, 2017
db9621c
Set SKU as default
TsuyoshiUshio Sep 13, 2017
357bc06
Follow the naming Rule with using location Sceme
TsuyoshiUshio Sep 22, 2017
ae309e0
Merge branch 'master' into functions-provider
TsuyoshiUshio Sep 22, 2017
d5ebef8
Use resourceGroupNameSchema()
TsuyoshiUshio Sep 23, 2017
75dc27c
Remove comment. I'll write the comment to the documentation.
TsuyoshiUshio Sep 23, 2017
1fe74ea
Adding SKU types validation
TsuyoshiUshio Sep 23, 2017
e86a8a2
Refactor: Remove getSku(). We don't need to write this, we can use Sk…
TsuyoshiUshio Sep 23, 2017
c3408b2
Follow the coding style.
TsuyoshiUshio Sep 23, 2017
2f74b8e
Remove unnecessary comment
TsuyoshiUshio Sep 23, 2017
4d4678f
Error message improvement to dump more for error struct.
TsuyoshiUshio Sep 23, 2017
011e38f
Make error more dump for error struct
TsuyoshiUshio Sep 23, 2017
c821a98
Using utility method.
TsuyoshiUshio Sep 23, 2017
577502d
Adding workspace name validation
TsuyoshiUshio Sep 23, 2017
3e8c7b5
Follow the naming rule
TsuyoshiUshio Sep 23, 2017
a578ba2
Adjust SDK version for operational insight
TsuyoshiUshio Sep 23, 2017
0cd140a
Change provider name and fix the Acceptance testing error
TsuyoshiUshio Sep 24, 2017
ea0692e
Adding the log anaylytics documentation
TsuyoshiUshio Sep 24, 2017
87b6e9a
Fix Travis-CI issue. It prevent make vet works.
TsuyoshiUshio Sep 24, 2017
9b64611
Edit the message for fit the error root cause
TsuyoshiUshio Sep 24, 2017
7c6ffaa
Changing name into OperationalInsight to LogAnalytics
TsuyoshiUshio Sep 25, 2017
a260e4f
Change the validation function to validation library
TsuyoshiUshio Sep 25, 2017
204c2fb
Fix in case of SKU is null
TsuyoshiUshio Sep 25, 2017
68bec07
Fix tyop and Fix the Operational Insight in Log messages
TsuyoshiUshio Sep 25, 2017
01fa92d
Fix Documentation hightlight.
TsuyoshiUshio Sep 25, 2017
5ced761
Comparing the Resource Group name in a case-insensitive manor
tombuildsstuff Sep 26, 2017
8e33ee6
Minor documentation cleanup
tombuildsstuff Sep 26, 2017
57b177f
`azurerm_log_analytics` -> `azurerm_log_analytics_workspace`
tombuildsstuff Sep 26, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/Azure/azure-sdk-for-go/arm/graphrbac"
"github.com/Azure/azure-sdk-for-go/arm/keyvault"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/Azure/azure-sdk-for-go/arm/operationalinsights"
"github.com/Azure/azure-sdk-for-go/arm/redis"
"github.com/Azure/azure-sdk-for-go/arm/resources/resources"
"github.com/Azure/azure-sdk-for-go/arm/scheduler"
Expand Down Expand Up @@ -87,6 +88,8 @@ type ArmClient struct {
eventHubConsumerGroupClient eventhub.ConsumerGroupsClient
eventHubNamespacesClient eventhub.NamespacesClient

workspacesClient operationalinsights.WorkspacesClient

providers resources.ProvidersClient
resourceGroupClient resources.GroupsClient
tagsClient resources.TagsClient
Expand Down Expand Up @@ -334,6 +337,12 @@ func (c *Config) getArmClient() (*ArmClient, error) {
lgc.Sender = autorest.CreateSender(withRequestLogging())
client.localNetConnClient = lgc

opwc := operationalinsights.NewWorkspacesClient(c.SubscriptionID)
setUserAgent(&opwc.Client)
opwc.Authorizer = auth
opwc.Sender = autorest.CreateSender(withRequestLogging())
client.workspacesClient = opwc

pipc := network.NewPublicIPAddressesClientWithBaseURI(endpoint, c.SubscriptionID)
setUserAgent(&pipc.Client)
pipc.Authorizer = auth
Expand Down
56 changes: 56 additions & 0 deletions azurerm/import_arm_operational_insight_workspace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package azurerm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given the resource has been renamed - can we rename this file to import_arm_log_analytics_workspace_test.go?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


import (
"testing"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)

func TestAccAzureRMOperationalInsightWorkspace_importrequiredOnly(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor we'd tend to name this _importRequiredOnly - could we update it to match?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

resourceName := "azurerm_operational_insight_workspace.test"

ri := acctest.RandInt()
config := testAccAzureRMOperationalInsightWorkspace_requiredOnly(ri, testLocation())

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMOperationalInsightWorkspaceDestroy,
Steps: []resource.TestStep{
{
Config: config,
},

{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccAzureRMOperationalInsightWorkspace_importoptional(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor we'd tend to name this _importOptional - could we update it to match?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

resourceName := "azurerm_operational_insight_workspace.test"

ri := acctest.RandInt()
config := testAccAzureRMOperationalInsightWorkspace_optional(ri, testLocation())

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMOperationalInsightWorkspaceDestroy,
Steps: []resource.TestStep{
{
Config: config,
},

{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
42 changes: 22 additions & 20 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,12 @@ func Provider() terraform.ResourceProvider {
"azurerm_lb_rule": resourceArmLoadBalancerRule(),
"azurerm_local_network_gateway": resourceArmLocalNetworkGateway(),

"azurerm_managed_disk": resourceArmManagedDisk(),
"azurerm_network_interface": resourceArmNetworkInterface(),
"azurerm_network_security_group": resourceArmNetworkSecurityGroup(),
"azurerm_network_security_rule": resourceArmNetworkSecurityRule(),
"azurerm_public_ip": resourceArmPublicIp(),
"azurerm_managed_disk": resourceArmManagedDisk(),
"azurerm_network_interface": resourceArmNetworkInterface(),
"azurerm_network_security_group": resourceArmNetworkSecurityGroup(),
"azurerm_network_security_rule": resourceArmNetworkSecurityRule(),
"azurerm_operational_insight_workspace": resourceArmOperationalInsightWorkspaceService(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused around the naming of this resource, is the latest product name for this "Operational Insights" or "Log Analytics"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also confused about that. I discuss with the production team. In conclusion, they'd love to go "Log Analytics" for terraform.tf. However, SDK and REST-API is "Operational Insights". I edit my code to fit to it.

"azurerm_public_ip": resourceArmPublicIp(),

"azurerm_redis_cache": resourceArmRedisCache(),
"azurerm_route": resourceArmRoute(),
Expand Down Expand Up @@ -254,21 +255,22 @@ func registerAzureResourceProvidersWithSubscription(providerList []resources.Pro
var err error
providerRegistrationOnce.Do(func() {
providers := map[string]struct{}{
"Microsoft.Cache": struct{}{},
"Microsoft.Cdn": struct{}{},
"Microsoft.Compute": struct{}{},
"Microsoft.ContainerRegistry": struct{}{},
"Microsoft.ContainerService": struct{}{},
"Microsoft.DocumentDB": struct{}{},
"Microsoft.EventHub": struct{}{},
"Microsoft.KeyVault": struct{}{},
"Microsoft.Insights": struct{}{},
"Microsoft.Network": struct{}{},
"Microsoft.Resources": struct{}{},
"Microsoft.Search": struct{}{},
"Microsoft.ServiceBus": struct{}{},
"Microsoft.Sql": struct{}{},
"Microsoft.Storage": struct{}{},
"Microsoft.Cache": struct{}{},
"Microsoft.Cdn": struct{}{},
"Microsoft.Compute": struct{}{},
"Microsoft.ContainerRegistry": struct{}{},
"Microsoft.ContainerService": struct{}{},
"Microsoft.DocumentDB": struct{}{},
"Microsoft.EventHub": struct{}{},
"Microsoft.KeyVault": struct{}{},
"Microsoft.Insights": struct{}{},
"Microsoft.Network": struct{}{},
"Microsoft.OperationalInsights": struct{}{},
"Microsoft.Resources": struct{}{},
"Microsoft.Search": struct{}{},
"Microsoft.ServiceBus": struct{}{},
"Microsoft.Sql": struct{}{},
"Microsoft.Storage": struct{}{},
}

// filter out any providers already registered
Expand Down
224 changes: 224 additions & 0 deletions azurerm/resource_arm_operational_insight_workspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package azurerm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as above) - could we rename this to resource_arm_log_analysis_workspace.go

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resource_arm_log_analytics_workspace.go instead. :) Done!


import (
"fmt"
"log"
"net/http"
"regexp"

"github.com/Azure/azure-sdk-for-go/arm/operationalinsights"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceArmOperationalInsightWorkspaceService() *schema.Resource {
return &schema.Resource{
Create: resourceArmOperationalInsightWorkspaceCreateUpdate,
Read: resourceArmOperationalInsightWorkspaceRead,
Update: resourceArmOperationalInsightWorkspaceCreateUpdate,
Delete: resourceArmOperationalInsightWorkspaceDelete,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above - could we rename this to LogAnalytics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateAzureRmOperationalInsightWorkspaceName,
},
"location": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: azureRMNormalizeLocation,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we swap location over to using the locationSchema? i.e. "location": locationSchema()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"resource_group_name": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as of earlier in the week there's now a resourceGroupNameSchema() - could we switch to using this instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this'll also fix the following test failure:

resource_group_name:  "acctestrg-5147622897607985308" => "acctestRG-5147622897607985308" (forces new resource)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I did it.

Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"workspace_id": { // a.k.a. customer_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor we'd tend to place this comment within the documentation e.g.

Exported attributes:

* `workspace_id` The Workspace ID / Customer ID for the Operational Insight Workspace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just remove this from code and add it on the documentation.

Type: schema.TypeString,
Computed: true,
},
"portal_url": {
Type: schema.TypeString,
Computed: true,
},
"sku": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add some validation to this for the possible SKU types, i.e.

ValidateFunc: validation.StringInSlice([]string{
  string(operationalinsights.Free),
  string(operationalinsights.PerNode),
  string(operationalinsights.Premium),
  string(operationalinsights.Standalone),
  string(operationalinsights.Standard),
  string(operationalinsights.Unlimited),
}, true),
DiffSuppressFunc: ignoreCaseDiffSuppressFunc,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DiffSuppressFunc included above should fix this test failure:

sku:                  "free" => "Free" (forces new resource)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! This is good one! I've done!

},
"retention_in_days": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"primary_shared_key": {
Type: schema.TypeString,
Computed: true,
},
"secondary_shared_key": {
Type: schema.TypeString,
Computed: true,
},
"tags": tagsSchema(),
},
}
}

func resourceArmOperationalInsightWorkspaceCreateUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).workspacesClient
log.Printf("[INFO] preparing arguments for AzureRM Operational Insight workspace creation.")

name := d.Get("name").(string)
location := d.Get("location").(string)
resGroup := d.Get("resource_group_name").(string)

skuName := d.Get("sku")
sku, err := getSku(skuName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we can just cast directly to the Enum, we can replace this with:

sku := &operationalinsights.Sku{
 Name: operationalinsights.SkuNameEnum(skuName),
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Also I removed the getSku and related function.

if err != nil {
return err
}

retentionInDays := int32(d.Get("retention_in_days").(int))

tags := d.Get("tags").(map[string]interface{})

parameters := operationalinsights.Workspace{
Name: &name,
Location: &location,
Tags: expandTags(tags),
WorkspaceProperties: &operationalinsights.WorkspaceProperties{
Sku: sku,
RetentionInDays: &retentionInDays,
},
}

cancel := make(chan struct{})
workspaceChannel, error := client.CreateOrUpdate(resGroup, name, parameters, cancel)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two things:

  1. can we in-line the cancel channel, since we're not making use of it at this time
  2. we'd normally ignore the response from the Create (in this case workspaceChannel) and instead do a Get request to get the object and it's ID (since the URI's could be different (as is commented out) - is there any reason why we've taken this route instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done.

I'm new to go-lang. However, I don't like copy-and-paste programming without understanding the reason. Since I just couldn't understand why you don't use the return value of CreateOrUpdate. Could you tell me the reason for my learning? :)

workspace := <-workspaceChannel
err = <-error
if err != nil {
return err
}
// The cosmos DB read rest api again for getting id. Try remove it.
// read, err := client.Get(resGroup, name)
// if err != nil {
// return err
//}

//if read.ID == nil {
// return fmt.Errorf("Cannot read Operational Inight Workspace '%s' (resource group %s) ID", name, resGroup)
//}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as above) - is there any reason why we're not re-requesting the resource here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I just forget to remove it. However, I follow your coding style, I mend it.

d.SetId(*workspace.ID)

return resourceArmOperationalInsightWorkspaceRead(d, meta)

}

func resourceArmOperationalInsightWorkspaceRead(d *schema.ResourceData, meta interface{}) error {
// I don't understand why we can get the meta data. How the framework set it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is set by the calls from Terraform Core - which injects the Provider as the meta argument, so you can access properties (in this case, such as the workspacesClient) without re-initializing it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the explanation! It help me to understand the behavior! Thanks. I removed the comment.

client := meta.(*ArmClient).workspacesClient
id, err := parseAzureResourceID(d.Id())
if err != nil {
return err
}
resGroup := id.ResourceGroup
name := id.Path["workspaces"]

resp, err := client.Get(resGroup, name)
if err != nil {
if responseWasNotFound(resp.Response) {
d.SetId("")
return nil
}
return fmt.Errorf("Error making Read request on AzureRM Operational Insight workspaces '%s': %s", name, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this second formatting argument %+v to return the full error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

d.Set("name", resp.Name)
d.Set("location", resp.Location)
d.Set("resource_group_name", resGroup)
d.Set("workspace_id", resp.CustomerID)
d.Set("portal_url", resp.PortalURL)
d.Set("sku", resp.Sku.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add a check to ensure that the returned SKU object isn't nil here? (this can happen when the Swagger doesn't match the API response, when the SDK is upgraded)

if sku := resp.Sku; sku != nil {
  d.Set("sku", sku.Name)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I didn't know that! Good info!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

d.Set("retention_in_days", resp.RetentionInDays)

sharedKeys, err := client.GetSharedKeys(resGroup, name)
if err != nil {
log.Printf("[ERROR] Unable to List Shared keys for Operatinal Insight workspaces %s: %s", name, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we update the secondary formatting argument to %+v to return the full error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

} else {
d.Set("primary_shared_key", sharedKeys.PrimarySharedKey)
d.Set("secondary_shared_key", sharedKeys.SecondarySharedKey)
}

flattenAndSetTags(d, resp.Tags)
return nil
}

func resourceArmOperationalInsightWorkspaceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).workspacesClient

id, err := parseAzureResourceID(d.Id())
if err != nil {
return err
}
resGroup := id.ResourceGroup
name := id.Path["workspaces"]

resp, err := client.Delete(resGroup, name)

if err != nil {
if resp.StatusCode == http.StatusNotFound {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we've got a helper method which also handles connection drops available in utils.ResponseWasNotFound(resp.Response) - can we swap this out for that instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

return nil
}

return fmt.Errorf("Error issuing AzureRM delete request for Operational Insight Workspaces '%s': %+v", name, err)
}

return nil
}

func getSku(skuName interface{}) (*operationalinsights.Sku, error) {
if skuName == nil {
return nil, nil
}
skuEnum, err := getSkuNameEnum(skuName.(string))
if err != nil {
return nil, err
}
return &operationalinsights.Sku{
Name: skuEnum,
}, nil
}

func getSkuNameEnum(skuName string) (operationalinsights.SkuNameEnum, error) {
switch skuName {
case "Free":
return operationalinsights.Free, nil
case "PerNode":
return operationalinsights.PerNode, nil
case "Premium":
return operationalinsights.Premium, nil
case "Standalone":
return operationalinsights.Standalone, nil
case "Standard":
return operationalinsights.Standard, nil
case "Unlimited":
return operationalinsights.Unlimited, nil
default:
return operationalinsights.Free, fmt.Errorf("Sku name not found")
}
}

func validateAzureRmOperationalInsightWorkspaceName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

r, _ := regexp.Compile("^[A-Za-z0-9][A-Za-z0-9-]+[A-Za-z0-9]$")
if !r.MatchString(value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see a max length listed in the API Documentation - is there one in the Portal we should add checks for?

Copy link
Contributor

@piotrgo piotrgo Sep 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name requirements are shown in the azure portal as a tooltip.

Workspace name should include 4-63 letters, digits or '-'. The '-' shouldn't be the first or the last symbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @piotrgo ! I added the validation with test.

errors = append(errors, fmt.Errorf("Workspace Name can only contain alphabet, number, and '-' charactor. You can not use '-' as the start and end of the name."))
}
return
}
Loading