546 lines
16 KiB
Go
546 lines
16 KiB
Go
|
package alicloud
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"github.com/denverdino/aliyungo/common"
|
||
|
"github.com/denverdino/aliyungo/rds"
|
||
|
"github.com/hashicorp/terraform/helper/hashcode"
|
||
|
"github.com/hashicorp/terraform/helper/resource"
|
||
|
"github.com/hashicorp/terraform/helper/schema"
|
||
|
"log"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
func resourceAlicloudDBInstance() *schema.Resource {
|
||
|
return &schema.Resource{
|
||
|
Create: resourceAlicloudDBInstanceCreate,
|
||
|
Read: resourceAlicloudDBInstanceRead,
|
||
|
Update: resourceAlicloudDBInstanceUpdate,
|
||
|
Delete: resourceAlicloudDBInstanceDelete,
|
||
|
|
||
|
Schema: map[string]*schema.Schema{
|
||
|
"engine": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue([]string{"MySQL", "SQLServer", "PostgreSQL", "PPAS"}),
|
||
|
ForceNew: true,
|
||
|
Required: true,
|
||
|
},
|
||
|
"engine_version": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue([]string{"5.5", "5.6", "5.7", "2008r2", "2012", "9.4", "9.3"}),
|
||
|
ForceNew: true,
|
||
|
Required: true,
|
||
|
},
|
||
|
"db_instance_class": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Required: true,
|
||
|
},
|
||
|
"db_instance_storage": &schema.Schema{
|
||
|
Type: schema.TypeInt,
|
||
|
Required: true,
|
||
|
},
|
||
|
|
||
|
"instance_charge_type": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue([]string{string(rds.Postpaid), string(rds.Prepaid)}),
|
||
|
Optional: true,
|
||
|
ForceNew: true,
|
||
|
Default: rds.Postpaid,
|
||
|
},
|
||
|
"period": &schema.Schema{
|
||
|
Type: schema.TypeInt,
|
||
|
ValidateFunc: validateAllowedIntValue([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 24, 36}),
|
||
|
Optional: true,
|
||
|
ForceNew: true,
|
||
|
Default: 1,
|
||
|
},
|
||
|
|
||
|
"zone_id": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Optional: true,
|
||
|
Computed: true,
|
||
|
},
|
||
|
"multi_az": &schema.Schema{
|
||
|
Type: schema.TypeBool,
|
||
|
Optional: true,
|
||
|
ForceNew: true,
|
||
|
},
|
||
|
"db_instance_net_type": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue([]string{string(common.Internet), string(common.Intranet)}),
|
||
|
Optional: true,
|
||
|
},
|
||
|
"allocate_public_connection": &schema.Schema{
|
||
|
Type: schema.TypeBool,
|
||
|
Optional: true,
|
||
|
Default: false,
|
||
|
},
|
||
|
|
||
|
"instance_network_type": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue([]string{string(common.VPC), string(common.Classic)}),
|
||
|
Optional: true,
|
||
|
Computed: true,
|
||
|
},
|
||
|
"vswitch_id": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ForceNew: true,
|
||
|
Optional: true,
|
||
|
},
|
||
|
|
||
|
"master_user_name": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ForceNew: true,
|
||
|
Optional: true,
|
||
|
},
|
||
|
"master_user_password": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ForceNew: true,
|
||
|
Optional: true,
|
||
|
Sensitive: true,
|
||
|
},
|
||
|
|
||
|
"preferred_backup_period": &schema.Schema{
|
||
|
Type: schema.TypeList,
|
||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||
|
// terraform does not support ValidateFunc of TypeList attr
|
||
|
// ValidateFunc: validateAllowedStringValue([]string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}),
|
||
|
Optional: true,
|
||
|
},
|
||
|
"preferred_backup_time": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue(rds.BACKUP_TIME),
|
||
|
Optional: true,
|
||
|
},
|
||
|
"backup_retention_period": &schema.Schema{
|
||
|
Type: schema.TypeInt,
|
||
|
ValidateFunc: validateIntegerInRange(7, 730),
|
||
|
Optional: true,
|
||
|
},
|
||
|
|
||
|
"security_ips": &schema.Schema{
|
||
|
Type: schema.TypeList,
|
||
|
Elem: &schema.Schema{Type: schema.TypeString},
|
||
|
Computed: true,
|
||
|
Optional: true,
|
||
|
},
|
||
|
|
||
|
"port": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Computed: true,
|
||
|
},
|
||
|
|
||
|
"connections": &schema.Schema{
|
||
|
Type: schema.TypeList,
|
||
|
Elem: &schema.Resource{
|
||
|
Schema: map[string]*schema.Schema{
|
||
|
"connection_string": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Required: true,
|
||
|
},
|
||
|
"ip_type": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Required: true,
|
||
|
},
|
||
|
"ip_address": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Optional: true,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
Computed: true,
|
||
|
},
|
||
|
|
||
|
"db_mappings": &schema.Schema{
|
||
|
Type: schema.TypeSet,
|
||
|
Elem: &schema.Resource{
|
||
|
Schema: map[string]*schema.Schema{
|
||
|
"db_name": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Required: true,
|
||
|
},
|
||
|
"character_set_name": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
ValidateFunc: validateAllowedStringValue(rds.CHARACTER_SET_NAME),
|
||
|
Required: true,
|
||
|
},
|
||
|
"db_description": &schema.Schema{
|
||
|
Type: schema.TypeString,
|
||
|
Optional: true,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
Optional: true,
|
||
|
Set: resourceAlicloudDatabaseHash,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func resourceAlicloudDatabaseHash(v interface{}) int {
|
||
|
var buf bytes.Buffer
|
||
|
m := v.(map[string]interface{})
|
||
|
buf.WriteString(fmt.Sprintf("%s-", m["db_name"].(string)))
|
||
|
buf.WriteString(fmt.Sprintf("%s-", m["character_set_name"].(string)))
|
||
|
buf.WriteString(fmt.Sprintf("%s-", m["db_description"].(string)))
|
||
|
|
||
|
return hashcode.String(buf.String())
|
||
|
}
|
||
|
|
||
|
func resourceAlicloudDBInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||
|
client := meta.(*AliyunClient)
|
||
|
conn := client.rdsconn
|
||
|
|
||
|
args, err := buildDBCreateOrderArgs(d, meta)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
resp, err := conn.CreateOrder(args)
|
||
|
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("Error creating Alicloud db instance: %#v", err)
|
||
|
}
|
||
|
|
||
|
instanceId := resp.DBInstanceId
|
||
|
if instanceId == "" {
|
||
|
return fmt.Errorf("Error get Alicloud db instance id")
|
||
|
}
|
||
|
|
||
|
d.SetId(instanceId)
|
||
|
d.Set("instance_charge_type", d.Get("instance_charge_type"))
|
||
|
d.Set("period", d.Get("period"))
|
||
|
d.Set("period_type", d.Get("period_type"))
|
||
|
|
||
|
// wait instance status change from Creating to running
|
||
|
if err := conn.WaitForInstance(d.Id(), rds.Running, defaultLongTimeout); err != nil {
|
||
|
log.Printf("[DEBUG] WaitForInstance %s got error: %#v", rds.Running, err)
|
||
|
}
|
||
|
|
||
|
if err := modifySecurityIps(d.Id(), d.Get("security_ips"), meta); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
masterUserName := d.Get("master_user_name").(string)
|
||
|
masterUserPwd := d.Get("master_user_password").(string)
|
||
|
if masterUserName != "" && masterUserPwd != "" {
|
||
|
if err := client.CreateAccountByInfo(d.Id(), masterUserName, masterUserPwd); err != nil {
|
||
|
return fmt.Errorf("Create db account %s error: %v", masterUserName, err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if d.Get("allocate_public_connection").(bool) {
|
||
|
if err := client.AllocateDBPublicConnection(d.Id(), DB_DEFAULT_CONNECT_PORT); err != nil {
|
||
|
return fmt.Errorf("Allocate public connection error: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return resourceAlicloudDBInstanceUpdate(d, meta)
|
||
|
}
|
||
|
|
||
|
func modifySecurityIps(id string, ips interface{}, meta interface{}) error {
|
||
|
client := meta.(*AliyunClient)
|
||
|
ipList := expandStringList(ips.([]interface{}))
|
||
|
|
||
|
ipstr := strings.Join(ipList[:], COMMA_SEPARATED)
|
||
|
// default disable connect from outside
|
||
|
if ipstr == "" {
|
||
|
ipstr = LOCAL_HOST_IP
|
||
|
}
|
||
|
|
||
|
if err := client.ModifyDBSecurityIps(id, ipstr); err != nil {
|
||
|
return fmt.Errorf("Error modify security ips %s: %#v", ipstr, err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func resourceAlicloudDBInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
||
|
client := meta.(*AliyunClient)
|
||
|
conn := client.rdsconn
|
||
|
d.Partial(true)
|
||
|
|
||
|
if d.HasChange("db_mappings") {
|
||
|
o, n := d.GetChange("db_mappings")
|
||
|
os := o.(*schema.Set)
|
||
|
ns := n.(*schema.Set)
|
||
|
|
||
|
var allDbs []string
|
||
|
remove := os.Difference(ns).List()
|
||
|
add := ns.Difference(os).List()
|
||
|
|
||
|
if len(remove) > 0 && len(add) > 0 {
|
||
|
return fmt.Errorf("Failure modify database, we neither support create and delete database simultaneous nor modify database attributes.")
|
||
|
}
|
||
|
|
||
|
if len(remove) > 0 {
|
||
|
for _, db := range remove {
|
||
|
dbm, _ := db.(map[string]interface{})
|
||
|
if err := conn.DeleteDatabase(d.Id(), dbm["db_name"].(string)); err != nil {
|
||
|
return fmt.Errorf("Failure delete database %s: %#v", dbm["db_name"].(string), err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(add) > 0 {
|
||
|
for _, db := range add {
|
||
|
dbm, _ := db.(map[string]interface{})
|
||
|
dbName := dbm["db_name"].(string)
|
||
|
allDbs = append(allDbs, dbName)
|
||
|
|
||
|
if err := client.CreateDatabaseByInfo(d.Id(), dbName, dbm["character_set_name"].(string), dbm["db_description"].(string)); err != nil {
|
||
|
return fmt.Errorf("Failure create database %s: %#v", dbName, err)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if err := conn.WaitForAllDatabase(d.Id(), allDbs, rds.Running, 600); err != nil {
|
||
|
return fmt.Errorf("Failure create database %#v", err)
|
||
|
}
|
||
|
|
||
|
if user := d.Get("master_user_name").(string); user != "" {
|
||
|
for _, dbName := range allDbs {
|
||
|
if err := client.GrantDBPrivilege2Account(d.Id(), user, dbName); err != nil {
|
||
|
return fmt.Errorf("Failed to grant database %s readwrite privilege to account %s: %#v", dbName, user, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
d.SetPartial("db_mappings")
|
||
|
}
|
||
|
|
||
|
if d.HasChange("preferred_backup_period") || d.HasChange("preferred_backup_time") || d.HasChange("backup_retention_period") {
|
||
|
period := d.Get("preferred_backup_period").([]interface{})
|
||
|
periodList := expandStringList(period)
|
||
|
time := d.Get("preferred_backup_time").(string)
|
||
|
retention := d.Get("backup_retention_period").(int)
|
||
|
|
||
|
if time == "" || retention == 0 || len(periodList) < 1 {
|
||
|
return fmt.Errorf("Both backup_time, backup_period and retention_period are required to set backup policy.")
|
||
|
}
|
||
|
|
||
|
ps := strings.Join(periodList[:], COMMA_SEPARATED)
|
||
|
|
||
|
if err := client.ConfigDBBackup(d.Id(), time, ps, retention); err != nil {
|
||
|
return fmt.Errorf("Error set backup policy: %#v", err)
|
||
|
}
|
||
|
d.SetPartial("preferred_backup_period")
|
||
|
d.SetPartial("preferred_backup_time")
|
||
|
d.SetPartial("backup_retention_period")
|
||
|
}
|
||
|
|
||
|
if d.HasChange("security_ips") {
|
||
|
if err := modifySecurityIps(d.Id(), d.Get("security_ips"), meta); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
d.SetPartial("security_ips")
|
||
|
}
|
||
|
|
||
|
if d.HasChange("db_instance_class") || d.HasChange("db_instance_storage") {
|
||
|
co, cn := d.GetChange("db_instance_class")
|
||
|
so, sn := d.GetChange("db_instance_storage")
|
||
|
classOld := co.(string)
|
||
|
classNew := cn.(string)
|
||
|
storageOld := so.(int)
|
||
|
storageNew := sn.(int)
|
||
|
|
||
|
// update except the first time, because we will do it in create function
|
||
|
if classOld != "" && storageOld != 0 {
|
||
|
chargeType := d.Get("instance_charge_type").(string)
|
||
|
if chargeType == string(rds.Prepaid) {
|
||
|
return fmt.Errorf("Prepaid db instance does not support modify db_instance_class or db_instance_storage")
|
||
|
}
|
||
|
|
||
|
if err := client.ModifyDBClassStorage(d.Id(), classNew, strconv.Itoa(storageNew)); err != nil {
|
||
|
return fmt.Errorf("Error modify db instance class or storage error: %#v", err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
d.Partial(false)
|
||
|
return resourceAlicloudDBInstanceRead(d, meta)
|
||
|
}
|
||
|
|
||
|
func resourceAlicloudDBInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||
|
client := meta.(*AliyunClient)
|
||
|
conn := client.rdsconn
|
||
|
|
||
|
instance, err := client.DescribeDBInstanceById(d.Id())
|
||
|
if err != nil {
|
||
|
if notFoundError(err) {
|
||
|
d.SetId("")
|
||
|
return nil
|
||
|
}
|
||
|
return fmt.Errorf("Error Describe DB InstanceAttribute: %#v", err)
|
||
|
}
|
||
|
|
||
|
args := rds.DescribeDatabasesArgs{
|
||
|
DBInstanceId: d.Id(),
|
||
|
}
|
||
|
|
||
|
resp, err := conn.DescribeDatabases(&args)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
d.Set("db_mappings", flattenDatabaseMappings(resp.Databases.Database))
|
||
|
|
||
|
argn := rds.DescribeDBInstanceNetInfoArgs{
|
||
|
DBInstanceId: d.Id(),
|
||
|
}
|
||
|
|
||
|
resn, err := conn.DescribeDBInstanceNetInfo(&argn)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
d.Set("connections", flattenDBConnections(resn.DBInstanceNetInfos.DBInstanceNetInfo))
|
||
|
|
||
|
ips, err := client.GetSecurityIps(d.Id())
|
||
|
if err != nil {
|
||
|
log.Printf("Describe DB security ips error: %#v", err)
|
||
|
}
|
||
|
d.Set("security_ips", ips)
|
||
|
|
||
|
d.Set("engine", instance.Engine)
|
||
|
d.Set("engine_version", instance.EngineVersion)
|
||
|
d.Set("db_instance_class", instance.DBInstanceClass)
|
||
|
d.Set("port", instance.Port)
|
||
|
d.Set("db_instance_storage", instance.DBInstanceStorage)
|
||
|
d.Set("zone_id", instance.ZoneId)
|
||
|
d.Set("db_instance_net_type", instance.DBInstanceNetType)
|
||
|
d.Set("instance_network_type", instance.InstanceNetworkType)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func resourceAlicloudDBInstanceDelete(d *schema.ResourceData, meta interface{}) error {
|
||
|
conn := meta.(*AliyunClient).rdsconn
|
||
|
|
||
|
return resource.Retry(5*time.Minute, func() *resource.RetryError {
|
||
|
err := conn.DeleteInstance(d.Id())
|
||
|
|
||
|
if err != nil {
|
||
|
return resource.RetryableError(fmt.Errorf("DB Instance in use - trying again while it is deleted."))
|
||
|
}
|
||
|
|
||
|
args := &rds.DescribeDBInstancesArgs{
|
||
|
DBInstanceId: d.Id(),
|
||
|
}
|
||
|
resp, err := conn.DescribeDBInstanceAttribute(args)
|
||
|
if err != nil {
|
||
|
return resource.NonRetryableError(err)
|
||
|
} else if len(resp.Items.DBInstanceAttribute) < 1 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return resource.RetryableError(fmt.Errorf("DB in use - trying again while it is deleted."))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func buildDBCreateOrderArgs(d *schema.ResourceData, meta interface{}) (*rds.CreateOrderArgs, error) {
|
||
|
client := meta.(*AliyunClient)
|
||
|
args := &rds.CreateOrderArgs{
|
||
|
RegionId: getRegion(d, meta),
|
||
|
// we does not expose this param to user,
|
||
|
// because create prepaid instance progress will be stopped when set auto_pay to false,
|
||
|
// then could not get instance info, cause timeout error
|
||
|
AutoPay: "true",
|
||
|
EngineVersion: d.Get("engine_version").(string),
|
||
|
Engine: rds.Engine(d.Get("engine").(string)),
|
||
|
DBInstanceStorage: d.Get("db_instance_storage").(int),
|
||
|
DBInstanceClass: d.Get("db_instance_class").(string),
|
||
|
Quantity: DEFAULT_INSTANCE_COUNT,
|
||
|
Resource: rds.DefaultResource,
|
||
|
}
|
||
|
|
||
|
bussStr, err := json.Marshal(DefaultBusinessInfo)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Failed to translate bussiness info %#v from json to string", DefaultBusinessInfo)
|
||
|
}
|
||
|
|
||
|
args.BusinessInfo = string(bussStr)
|
||
|
|
||
|
zoneId := d.Get("zone_id").(string)
|
||
|
args.ZoneId = zoneId
|
||
|
|
||
|
multiAZ := d.Get("multi_az").(bool)
|
||
|
if multiAZ {
|
||
|
if zoneId != "" {
|
||
|
return nil, fmt.Errorf("You cannot set the ZoneId parameter when the MultiAZ parameter is set to true")
|
||
|
}
|
||
|
izs, err := client.DescribeMultiIZByRegion()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Get multiAZ id error")
|
||
|
}
|
||
|
|
||
|
if len(izs) < 1 {
|
||
|
return nil, fmt.Errorf("Current region does not support MultiAZ.")
|
||
|
}
|
||
|
|
||
|
args.ZoneId = izs[0]
|
||
|
}
|
||
|
|
||
|
vswitchId := d.Get("vswitch_id").(string)
|
||
|
|
||
|
networkType := d.Get("instance_network_type").(string)
|
||
|
args.InstanceNetworkType = common.NetworkType(networkType)
|
||
|
|
||
|
if vswitchId != "" {
|
||
|
args.VSwitchId = vswitchId
|
||
|
|
||
|
// check InstanceNetworkType with vswitchId
|
||
|
if networkType == string(common.Classic) {
|
||
|
return nil, fmt.Errorf("When fill vswitchId, you shold set instance_network_type to VPC")
|
||
|
} else if networkType == "" {
|
||
|
args.InstanceNetworkType = common.VPC
|
||
|
}
|
||
|
|
||
|
// get vpcId
|
||
|
vpcId, err := client.GetVpcIdByVSwitchId(vswitchId)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("VswitchId %s is not valid of current region", vswitchId)
|
||
|
}
|
||
|
// fill vpcId by vswitchId
|
||
|
args.VPCId = vpcId
|
||
|
|
||
|
// check vswitchId in zone
|
||
|
vsw, err := client.QueryVswitchById(vpcId, vswitchId)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("VswitchId %s is not valid of current region", vswitchId)
|
||
|
}
|
||
|
|
||
|
if zoneId == "" {
|
||
|
args.ZoneId = vsw.ZoneId
|
||
|
} else if vsw.ZoneId != zoneId {
|
||
|
return nil, fmt.Errorf("VswitchId %s is not belong to the zone %s", vswitchId, zoneId)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if v := d.Get("db_instance_net_type").(string); v != "" {
|
||
|
args.DBInstanceNetType = common.NetType(v)
|
||
|
}
|
||
|
|
||
|
chargeType := d.Get("instance_charge_type").(string)
|
||
|
if chargeType != "" {
|
||
|
args.PayType = rds.DBPayType(chargeType)
|
||
|
} else {
|
||
|
args.PayType = rds.Postpaid
|
||
|
}
|
||
|
|
||
|
// if charge type is postpaid, the commodity code must set to bards
|
||
|
if chargeType == string(rds.Postpaid) {
|
||
|
args.CommodityCode = rds.Bards
|
||
|
} else {
|
||
|
args.CommodityCode = rds.Rds
|
||
|
}
|
||
|
|
||
|
period := d.Get("period").(int)
|
||
|
args.UsedTime, args.TimeType = TransformPeriod2Time(period, chargeType)
|
||
|
|
||
|
return args, nil
|
||
|
}
|