provider/aws: Add support for targets to aws_ssm_association (#14246)
* provider/aws: Add support for targets to aws_ssm_association Fixes: #13975 ``` % make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSSSMAssociation_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2017/05/05 20:32:43 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSSSMAssociation_ -timeout 120m === RUN TestAccAWSSSMAssociation_basic --- PASS: TestAccAWSSSMAssociation_basic (139.13s) === RUN TestAccAWSSSMAssociation_withTargets --- PASS: TestAccAWSSSMAssociation_withTargets (33.19s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws 172.343s ``` * Update ssm_association.html.markdown
This commit is contained in:
parent
3230217dc5
commit
9dd4e5bcb0
|
@ -17,22 +17,46 @@ func resourceAwsSsmAssociation() *schema.Resource {
|
||||||
Delete: resourceAwsSsmAssociationDelete,
|
Delete: resourceAwsSsmAssociationDelete,
|
||||||
|
|
||||||
Schema: map[string]*schema.Schema{
|
Schema: map[string]*schema.Schema{
|
||||||
|
"association_id": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
"instance_id": {
|
"instance_id": {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
Required: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
Type: schema.TypeString,
|
Type: schema.TypeString,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
"parameters": &schema.Schema{
|
"parameters": {
|
||||||
Type: schema.TypeMap,
|
Type: schema.TypeMap,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
ForceNew: true,
|
ForceNew: true,
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
"targets": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Optional: true,
|
||||||
|
ForceNew: true,
|
||||||
|
Computed: true,
|
||||||
|
MaxItems: 1,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"key": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Required: true,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,14 +67,21 @@ func resourceAwsSsmAssociationCreate(d *schema.ResourceData, meta interface{}) e
|
||||||
log.Printf("[DEBUG] SSM association create: %s", d.Id())
|
log.Printf("[DEBUG] SSM association create: %s", d.Id())
|
||||||
|
|
||||||
assosciationInput := &ssm.CreateAssociationInput{
|
assosciationInput := &ssm.CreateAssociationInput{
|
||||||
Name: aws.String(d.Get("name").(string)),
|
Name: aws.String(d.Get("name").(string)),
|
||||||
InstanceId: aws.String(d.Get("instance_id").(string)),
|
}
|
||||||
|
|
||||||
|
if v, ok := d.GetOk("instance_id"); ok {
|
||||||
|
assosciationInput.InstanceId = aws.String(v.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := d.GetOk("parameters"); ok {
|
if v, ok := d.GetOk("parameters"); ok {
|
||||||
assosciationInput.Parameters = expandSSMDocumentParameters(v.(map[string]interface{}))
|
assosciationInput.Parameters = expandSSMDocumentParameters(v.(map[string]interface{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := d.GetOk("targets"); ok {
|
||||||
|
assosciationInput.Targets = expandAwsSsmTargets(d)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := ssmconn.CreateAssociation(assosciationInput)
|
resp, err := ssmconn.CreateAssociation(assosciationInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrapf("[ERROR] Error creating SSM association: {{err}}", err)
|
return errwrap.Wrapf("[ERROR] Error creating SSM association: {{err}}", err)
|
||||||
|
@ -61,6 +92,7 @@ func resourceAwsSsmAssociationCreate(d *schema.ResourceData, meta interface{}) e
|
||||||
}
|
}
|
||||||
|
|
||||||
d.SetId(*resp.AssociationDescription.Name)
|
d.SetId(*resp.AssociationDescription.Name)
|
||||||
|
d.Set("association_id", resp.AssociationDescription.AssociationId)
|
||||||
|
|
||||||
return resourceAwsSsmAssociationRead(d, meta)
|
return resourceAwsSsmAssociationRead(d, meta)
|
||||||
}
|
}
|
||||||
|
@ -68,11 +100,10 @@ func resourceAwsSsmAssociationCreate(d *schema.ResourceData, meta interface{}) e
|
||||||
func resourceAwsSsmAssociationRead(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsSsmAssociationRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
ssmconn := meta.(*AWSClient).ssmconn
|
ssmconn := meta.(*AWSClient).ssmconn
|
||||||
|
|
||||||
log.Printf("[DEBUG] Reading SSM Assosciation: %s", d.Id())
|
log.Printf("[DEBUG] Reading SSM Association: %s", d.Id())
|
||||||
|
|
||||||
params := &ssm.DescribeAssociationInput{
|
params := &ssm.DescribeAssociationInput{
|
||||||
Name: aws.String(d.Get("name").(string)),
|
AssociationId: aws.String(d.Get("association_id").(string)),
|
||||||
InstanceId: aws.String(d.Get("instance_id").(string)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := ssmconn.DescribeAssociation(params)
|
resp, err := ssmconn.DescribeAssociation(params)
|
||||||
|
@ -88,6 +119,11 @@ func resourceAwsSsmAssociationRead(d *schema.ResourceData, meta interface{}) err
|
||||||
d.Set("instance_id", association.InstanceId)
|
d.Set("instance_id", association.InstanceId)
|
||||||
d.Set("name", association.Name)
|
d.Set("name", association.Name)
|
||||||
d.Set("parameters", association.Parameters)
|
d.Set("parameters", association.Parameters)
|
||||||
|
d.Set("association_id", association.AssociationId)
|
||||||
|
|
||||||
|
if err := d.Set("targets", flattenAwsSsmTargets(association.Targets)); err != nil {
|
||||||
|
return fmt.Errorf("[DEBUG] Error setting targets error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -98,8 +134,7 @@ func resourceAwsSsmAssociationDelete(d *schema.ResourceData, meta interface{}) e
|
||||||
log.Printf("[DEBUG] Deleting SSM Assosciation: %s", d.Id())
|
log.Printf("[DEBUG] Deleting SSM Assosciation: %s", d.Id())
|
||||||
|
|
||||||
params := &ssm.DeleteAssociationInput{
|
params := &ssm.DeleteAssociationInput{
|
||||||
Name: aws.String(d.Get("name").(string)),
|
AssociationId: aws.String(d.Get("association_id").(string)),
|
||||||
InstanceId: aws.String(d.Get("instance_id").(string)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ssmconn.DeleteAssociation(params)
|
_, err := ssmconn.DeleteAssociation(params)
|
||||||
|
|
|
@ -19,7 +19,7 @@ func TestAccAWSSSMAssociation_basic(t *testing.T) {
|
||||||
Providers: testAccProviders,
|
Providers: testAccProviders,
|
||||||
CheckDestroy: testAccCheckAWSSSMAssociationDestroy,
|
CheckDestroy: testAccCheckAWSSSMAssociationDestroy,
|
||||||
Steps: []resource.TestStep{
|
Steps: []resource.TestStep{
|
||||||
resource.TestStep{
|
{
|
||||||
Config: testAccAWSSSMAssociationBasicConfig(name),
|
Config: testAccAWSSSMAssociationBasicConfig(name),
|
||||||
Check: resource.ComposeTestCheckFunc(
|
Check: resource.ComposeTestCheckFunc(
|
||||||
testAccCheckAWSSSMAssociationExists("aws_ssm_association.foo"),
|
testAccCheckAWSSSMAssociationExists("aws_ssm_association.foo"),
|
||||||
|
@ -29,6 +29,23 @@ func TestAccAWSSSMAssociation_basic(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccAWSSSMAssociation_withTargets(t *testing.T) {
|
||||||
|
name := acctest.RandString(10)
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSSSMAssociationDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccAWSSSMAssociationBasicConfigWithTargets(name),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckAWSSSMAssociationExists("aws_ssm_association.foo"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testAccCheckAWSSSMAssociationExists(n string) resource.TestCheckFunc {
|
func testAccCheckAWSSSMAssociationExists(n string) resource.TestCheckFunc {
|
||||||
return func(s *terraform.State) error {
|
return func(s *terraform.State) error {
|
||||||
rs, ok := s.RootModule().Resources[n]
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
@ -43,12 +60,14 @@ func testAccCheckAWSSSMAssociationExists(n string) resource.TestCheckFunc {
|
||||||
conn := testAccProvider.Meta().(*AWSClient).ssmconn
|
conn := testAccProvider.Meta().(*AWSClient).ssmconn
|
||||||
|
|
||||||
_, err := conn.DescribeAssociation(&ssm.DescribeAssociationInput{
|
_, err := conn.DescribeAssociation(&ssm.DescribeAssociationInput{
|
||||||
Name: aws.String(rs.Primary.Attributes["name"]),
|
AssociationId: aws.String(rs.Primary.Attributes["association_id"]),
|
||||||
InstanceId: aws.String(rs.Primary.Attributes["instance_id"]),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Could not descripbe the assosciation - %s", err)
|
if wserr, ok := err.(awserr.Error); ok && wserr.Code() == "AssociationDoesNotExist" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -64,24 +83,57 @@ func testAccCheckAWSSSMAssociationDestroy(s *terraform.State) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := conn.DescribeAssociation(&ssm.DescribeAssociationInput{
|
out, err := conn.DescribeAssociation(&ssm.DescribeAssociationInput{
|
||||||
Name: aws.String(rs.Primary.Attributes["name"]),
|
AssociationId: aws.String(rs.Primary.Attributes["association_id"]),
|
||||||
InstanceId: aws.String(rs.Primary.Attributes["instance_id"]),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// InvalidDocument means it's gone, this is good
|
if wserr, ok := err.(awserr.Error); ok && wserr.Code() == "AssociationDoesNotExist" {
|
||||||
if wserr, ok := err.(awserr.Error); ok && wserr.Code() == "InvalidDocument" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if out != nil {
|
if out != nil {
|
||||||
return fmt.Errorf("Expected AWS SSM Assosciation to be gone, but was still found")
|
return fmt.Errorf("Expected AWS SSM Association to be gone, but was still found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Default error in SSM Assosciation Test")
|
return fmt.Errorf("Default error in SSM Association Test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccAWSSSMAssociationBasicConfigWithTargets(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "aws_ssm_document" "foo_document" {
|
||||||
|
name = "test_document_association-%s",
|
||||||
|
document_type = "Command"
|
||||||
|
content = <<DOC
|
||||||
|
{
|
||||||
|
"schemaVersion": "1.2",
|
||||||
|
"description": "Check ip configuration of a Linux instance.",
|
||||||
|
"parameters": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"runtimeConfig": {
|
||||||
|
"aws:runShellScript": {
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "0.aws:runShellScript",
|
||||||
|
"runCommand": ["ifconfig"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DOC
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_ssm_association" "foo" {
|
||||||
|
name = "${aws_ssm_document.foo_document.name}",
|
||||||
|
targets {
|
||||||
|
key = "tag:Name"
|
||||||
|
values = ["acceptanceTest"]
|
||||||
|
}
|
||||||
|
}`, rName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccAWSSSMAssociationBasicConfig(rName string) string {
|
func testAccAWSSSMAssociationBasicConfig(rName string) string {
|
||||||
|
|
|
@ -57,42 +57,6 @@ func resourceAwsSsmMaintenanceWindowTarget() *schema.Resource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandAwsSsmMaintenanceWindowTargets(d *schema.ResourceData) []*ssm.Target {
|
|
||||||
var targets []*ssm.Target
|
|
||||||
|
|
||||||
targetConfig := d.Get("targets").([]interface{})
|
|
||||||
|
|
||||||
for _, tConfig := range targetConfig {
|
|
||||||
config := tConfig.(map[string]interface{})
|
|
||||||
|
|
||||||
target := &ssm.Target{
|
|
||||||
Key: aws.String(config["key"].(string)),
|
|
||||||
Values: expandStringList(config["values"].([]interface{})),
|
|
||||||
}
|
|
||||||
|
|
||||||
targets = append(targets, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets
|
|
||||||
}
|
|
||||||
|
|
||||||
func flattenAwsSsmMaintenanceWindowTargets(targets []*ssm.Target) []map[string]interface{} {
|
|
||||||
if len(targets) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]map[string]interface{}, 0, len(targets))
|
|
||||||
target := targets[0]
|
|
||||||
|
|
||||||
t := make(map[string]interface{})
|
|
||||||
t["key"] = *target.Key
|
|
||||||
t["values"] = flattenStringList(target.Values)
|
|
||||||
|
|
||||||
result = append(result, t)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func resourceAwsSsmMaintenanceWindowTargetCreate(d *schema.ResourceData, meta interface{}) error {
|
func resourceAwsSsmMaintenanceWindowTargetCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
ssmconn := meta.(*AWSClient).ssmconn
|
ssmconn := meta.(*AWSClient).ssmconn
|
||||||
|
|
||||||
|
@ -101,7 +65,7 @@ func resourceAwsSsmMaintenanceWindowTargetCreate(d *schema.ResourceData, meta in
|
||||||
params := &ssm.RegisterTargetWithMaintenanceWindowInput{
|
params := &ssm.RegisterTargetWithMaintenanceWindowInput{
|
||||||
WindowId: aws.String(d.Get("window_id").(string)),
|
WindowId: aws.String(d.Get("window_id").(string)),
|
||||||
ResourceType: aws.String(d.Get("resource_type").(string)),
|
ResourceType: aws.String(d.Get("resource_type").(string)),
|
||||||
Targets: expandAwsSsmMaintenanceWindowTargets(d),
|
Targets: expandAwsSsmTargets(d),
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := d.GetOk("owner_information"); ok {
|
if v, ok := d.GetOk("owner_information"); ok {
|
||||||
|
@ -145,7 +109,7 @@ func resourceAwsSsmMaintenanceWindowTargetRead(d *schema.ResourceData, meta inte
|
||||||
d.Set("window_id", t.WindowId)
|
d.Set("window_id", t.WindowId)
|
||||||
d.Set("resource_type", t.ResourceType)
|
d.Set("resource_type", t.ResourceType)
|
||||||
|
|
||||||
if err := d.Set("targets", flattenAwsSsmMaintenanceWindowTargets(t.Targets)); err != nil {
|
if err := d.Set("targets", flattenAwsSsmTargets(t.Targets)); err != nil {
|
||||||
return fmt.Errorf("[DEBUG] Error setting targets error: %#v", err)
|
return fmt.Errorf("[DEBUG] Error setting targets error: %#v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ func resourceAwsSsmMaintenanceWindowTaskCreate(d *schema.ResourceData, meta inte
|
||||||
TaskType: aws.String(d.Get("task_type").(string)),
|
TaskType: aws.String(d.Get("task_type").(string)),
|
||||||
ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)),
|
ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)),
|
||||||
TaskArn: aws.String(d.Get("task_arn").(string)),
|
TaskArn: aws.String(d.Get("task_arn").(string)),
|
||||||
Targets: expandAwsSsmMaintenanceWindowTargets(d),
|
Targets: expandAwsSsmTargets(d),
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := d.GetOk("priority"); ok {
|
if v, ok := d.GetOk("priority"); ok {
|
||||||
|
@ -196,7 +196,7 @@ func resourceAwsSsmMaintenanceWindowTaskRead(d *schema.ResourceData, meta interf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.Set("targets", flattenAwsSsmMaintenanceWindowTargets(t.Targets)); err != nil {
|
if err := d.Set("targets", flattenAwsSsmTargets(t.Targets)); err != nil {
|
||||||
return fmt.Errorf("[DEBUG] Error setting targets error: %#v", err)
|
return fmt.Errorf("[DEBUG] Error setting targets error: %#v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/aws/aws-sdk-go/service/rds"
|
"github.com/aws/aws-sdk-go/service/rds"
|
||||||
"github.com/aws/aws-sdk-go/service/redshift"
|
"github.com/aws/aws-sdk-go/service/redshift"
|
||||||
"github.com/aws/aws-sdk-go/service/route53"
|
"github.com/aws/aws-sdk-go/service/route53"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ssm"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
@ -2085,3 +2086,39 @@ func sliceContainsMap(l []interface{}, m map[string]interface{}) (int, bool) {
|
||||||
|
|
||||||
return -1, false
|
return -1, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandAwsSsmTargets(d *schema.ResourceData) []*ssm.Target {
|
||||||
|
var targets []*ssm.Target
|
||||||
|
|
||||||
|
targetConfig := d.Get("targets").([]interface{})
|
||||||
|
|
||||||
|
for _, tConfig := range targetConfig {
|
||||||
|
config := tConfig.(map[string]interface{})
|
||||||
|
|
||||||
|
target := &ssm.Target{
|
||||||
|
Key: aws.String(config["key"].(string)),
|
||||||
|
Values: expandStringList(config["values"].([]interface{})),
|
||||||
|
}
|
||||||
|
|
||||||
|
targets = append(targets, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenAwsSsmTargets(targets []*ssm.Target) []map[string]interface{} {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]map[string]interface{}, 0, len(targets))
|
||||||
|
target := targets[0]
|
||||||
|
|
||||||
|
t := make(map[string]interface{})
|
||||||
|
t["key"] = *target.Key
|
||||||
|
t["values"] = flattenStringList(target.Values)
|
||||||
|
|
||||||
|
result = append(result, t)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ description: |-
|
||||||
Assosciates an SSM Document to an instance.
|
Assosciates an SSM Document to an instance.
|
||||||
---
|
---
|
||||||
|
|
||||||
# aws\_ssm\_association
|
# aws_ssm_association
|
||||||
|
|
||||||
Assosciates an SSM Document to an instance.
|
Assosciates an SSM Document to an instance.
|
||||||
|
|
||||||
|
@ -68,8 +68,9 @@ resource "aws_ssm_association" "foo" {
|
||||||
The following arguments are supported:
|
The following arguments are supported:
|
||||||
|
|
||||||
* `name` - (Required) The name of the SSM document to apply.
|
* `name` - (Required) The name of the SSM document to apply.
|
||||||
* `instance_id` - (Required) The instance id to apply an SSM document to.
|
* `instance_id` - (Optional) The instance id to apply an SSM document to.
|
||||||
* `parameters` - (Optional) Additional parameters to pass to the SSM document.
|
* `parameters` - (Optional) Additional parameters to pass to the SSM document.
|
||||||
|
* `targets` - (Optional) The targets (either instances or tags). Instances are specified using Key=instanceids,Values=instanceid1,instanceid2. Tags are specified using Key=tag name,Values=tag value. Only 1 target is currently supported by AWS.
|
||||||
|
|
||||||
## Attributes Reference
|
## Attributes Reference
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue