mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-08 06:31:00 +08:00
575 lines
17 KiB
Go
575 lines
17 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cloudprovider
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/fatih/set.v0"
|
|
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/util/secrules"
|
|
"yunion.io/x/pkg/utils"
|
|
)
|
|
|
|
type SecurityGroupReference struct {
|
|
Id string
|
|
Name string
|
|
}
|
|
|
|
type SecDriver interface {
|
|
GetDefaultSecurityGroupInRule() SecurityRule
|
|
GetDefaultSecurityGroupOutRule() SecurityRule
|
|
GetSecurityGroupRuleMaxPriority() int
|
|
GetSecurityGroupRuleMinPriority() int
|
|
IsOnlySupportAllowRules() bool
|
|
IsSupportPeerSecgroup() bool
|
|
}
|
|
|
|
func NewSecRuleInfo(driver SecDriver) SecRuleInfo {
|
|
return SecRuleInfo{
|
|
InDefaultRule: driver.GetDefaultSecurityGroupInRule(),
|
|
OutDefaultRule: driver.GetDefaultSecurityGroupOutRule(),
|
|
MinPriority: driver.GetSecurityGroupRuleMinPriority(),
|
|
MaxPriority: driver.GetSecurityGroupRuleMaxPriority(),
|
|
IsOnlySupportAllowRules: driver.IsOnlySupportAllowRules(),
|
|
IsSupportPeerSecgroup: driver.IsSupportPeerSecgroup(),
|
|
}
|
|
}
|
|
|
|
const DEFAULT_DEST_RULE_ID = "default_dest_rule_id"
|
|
const DEFAULT_SRC_RULE_ID = "default_src_rule_id"
|
|
|
|
type SecRuleInfo struct {
|
|
InDefaultRule SecurityRule
|
|
OutDefaultRule SecurityRule
|
|
in SecurityRuleSet
|
|
out SecurityRuleSet
|
|
rules SecurityRuleSet
|
|
Rules SecurityRuleSet
|
|
MinPriority int
|
|
MaxPriority int
|
|
IsOnlySupportAllowRules bool
|
|
IsSupportPeerSecgroup bool
|
|
}
|
|
|
|
func (r SecRuleInfo) getOffest(priority int) (int, int) {
|
|
if r.MinPriority < r.MaxPriority {
|
|
if priority >= r.MaxPriority {
|
|
return priority, -1
|
|
}
|
|
return priority + 1, 0
|
|
} else {
|
|
if priority <= r.MaxPriority {
|
|
return priority, 1
|
|
}
|
|
return priority - 1, 0
|
|
}
|
|
}
|
|
|
|
func (r SecRuleInfo) AddDefaultRule(d SecRuleInfo, inRules, outRules []SecurityRule, isSrc bool) ([]SecurityRule, []SecurityRule) {
|
|
min, max := r.MinPriority, r.MaxPriority
|
|
r.InDefaultRule.Priority = min + 1
|
|
r.OutDefaultRule.Priority = min + 1
|
|
r.InDefaultRule.minPriority, r.InDefaultRule.maxPriority = r.MinPriority, r.MaxPriority
|
|
r.OutDefaultRule.minPriority, r.OutDefaultRule.maxPriority = r.MinPriority, r.MaxPriority
|
|
if max >= min {
|
|
r.InDefaultRule.Priority = min - 1
|
|
r.OutDefaultRule.Priority = min - 1
|
|
}
|
|
|
|
if isSrc {
|
|
r.InDefaultRule.Id = DEFAULT_SRC_RULE_ID
|
|
r.OutDefaultRule.Id = DEFAULT_SRC_RULE_ID
|
|
} else {
|
|
r.InDefaultRule.ExternalId = DEFAULT_DEST_RULE_ID
|
|
r.OutDefaultRule.ExternalId = DEFAULT_DEST_RULE_ID
|
|
}
|
|
|
|
var isWideRule = func(rules []SecurityRule) bool {
|
|
for _, rule := range rules {
|
|
if strings.HasSuffix(rule.String(), "allow any") || strings.HasSuffix(rule.String(), "deny any") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if !isWideRule(inRules) {
|
|
inRules = append(inRules, r.InDefaultRule)
|
|
}
|
|
if !isWideRule(outRules) {
|
|
outRules = append(outRules, r.OutDefaultRule)
|
|
}
|
|
return inRules, outRules
|
|
}
|
|
|
|
type SecurityGroupFilterOptions struct {
|
|
VpcId string
|
|
Name string
|
|
ProjectId string
|
|
}
|
|
|
|
type SecurityGroupCreateInput struct {
|
|
Name string
|
|
Desc string
|
|
VpcId string
|
|
ProjectId string
|
|
Rules []secrules.SecurityRule
|
|
}
|
|
|
|
type SecurityGroupCreateOutput struct {
|
|
Return bool
|
|
GroupId string
|
|
}
|
|
|
|
type SecurityRule struct {
|
|
minPriority int
|
|
maxPriority int
|
|
offset int
|
|
|
|
secrules.SecurityRule
|
|
Name string
|
|
ExternalId string
|
|
Id string
|
|
|
|
PeerSecgroupId string
|
|
}
|
|
|
|
func (self *SecurityRule) addPriority(offset int) {
|
|
self.Priority += offset
|
|
self.offset += offset
|
|
}
|
|
|
|
func (r SecurityRule) String() string {
|
|
if len(r.PeerSecgroupId) == 0 {
|
|
return r.SecurityRule.String()
|
|
}
|
|
return fmt.Sprintf("%s-%s", r.SecurityRule.String(), r.PeerSecgroupId)
|
|
}
|
|
|
|
type SecurityRuleSet []SecurityRule
|
|
|
|
func (self *SecRuleInfo) Split() (in, out SecurityRuleSet) {
|
|
self.rules = SecurityRuleSet{}
|
|
self.in = SecurityRuleSet{}
|
|
self.out = SecurityRuleSet{}
|
|
for i := 0; i < len(self.Rules); i++ {
|
|
self.rules = append(self.rules, self.Rules[i])
|
|
if self.Rules[i].Direction == secrules.DIR_IN {
|
|
self.in = append(self.in, self.Rules[i])
|
|
} else {
|
|
self.out = append(self.out, self.Rules[i])
|
|
}
|
|
self.Rules[i].minPriority, self.Rules[i].maxPriority = self.MinPriority, self.MaxPriority
|
|
if !self.IsSupportPeerSecgroup && len(self.Rules[i].PeerSecgroupId) > 0 {
|
|
continue
|
|
}
|
|
if self.Rules[i].Direction == secrules.DIR_IN {
|
|
in = append(in, self.Rules[i])
|
|
} else {
|
|
out = append(out, self.Rules[i])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (srs SecurityRuleSet) Len() int {
|
|
return len(srs)
|
|
}
|
|
|
|
func (srs SecurityRuleSet) Swap(i, j int) {
|
|
srs[i], srs[j] = srs[j], srs[i]
|
|
}
|
|
|
|
func (srs SecurityRuleSet) Less(i, j int) bool {
|
|
if srs[i].Priority < srs[j].Priority {
|
|
return true
|
|
}
|
|
if srs[i].Priority > srs[j].Priority {
|
|
return false
|
|
}
|
|
if len(srs) > 0 {
|
|
if (srs[0].minPriority <= srs[0].maxPriority && srs[i].String() < srs[j].String()) ||
|
|
(srs[0].minPriority > srs[0].maxPriority && srs[i].String() > srs[j].String()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (srs SecurityRuleSet) CanBeSplitByProtocol() bool {
|
|
firstNormalRuleIndex, find := 0, false
|
|
for idx, r := range srs {
|
|
if !(strings.HasSuffix(r.String(), "allow any") || strings.HasSuffix(r.String(), "deny any")) && !find {
|
|
firstNormalRuleIndex = idx
|
|
find = true
|
|
} else {
|
|
if r.ExternalId == DEFAULT_SRC_RULE_ID || r.ExternalId == DEFAULT_DEST_RULE_ID {
|
|
return true
|
|
}
|
|
if idx > firstNormalRuleIndex {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (srs SecurityRuleSet) AllowList() (secrules.SecurityRuleSet, bool) {
|
|
rules := secrules.SecurityRuleSet{}
|
|
isOk := true
|
|
for _, r := range srs {
|
|
if len(r.PeerSecgroupId) > 0 {
|
|
isOk = false
|
|
}
|
|
rules = append(rules, r.SecurityRule)
|
|
}
|
|
return rules.AllowList(), isOk
|
|
}
|
|
|
|
func (srs SecurityRuleSet) Debug() {
|
|
for i := 0; i < len(srs); i++ {
|
|
log.Debugf("Name: %s id: %s external_id: %s min: %d max: %d priority: %d %s", srs[i].Name, srs[i].Id, srs[i].ExternalId, srs[i].minPriority, srs[i].maxPriority, srs[i].Priority, srs[i].String())
|
|
}
|
|
}
|
|
|
|
func SortSecurityRule(rules SecurityRuleSet, isAsc bool, info SecRuleInfo) {
|
|
if (info.MaxPriority > info.MinPriority && isAsc) || (info.MaxPriority < info.MinPriority && !isAsc) || (info.IsOnlySupportAllowRules && isAsc) {
|
|
sort.Sort(rules)
|
|
return
|
|
}
|
|
sort.Sort(sort.Reverse(rules))
|
|
return
|
|
}
|
|
|
|
func isAllowListEqual(src, dest secrules.SecurityRuleSet) bool {
|
|
if len(src) != len(dest) {
|
|
return false
|
|
}
|
|
s1, s2 := set.New(set.ThreadSafe), set.New(set.ThreadSafe)
|
|
for i := 0; i < len(src); i++ {
|
|
s1.Add(src[i].String())
|
|
s2.Add(dest[i].String())
|
|
}
|
|
return s1.IsEqual(s2)
|
|
}
|
|
|
|
func isPeerListEqual(src, dest SecurityRuleSet) bool {
|
|
if len(src) != len(dest) {
|
|
return false
|
|
}
|
|
s1, s2 := set.New(set.ThreadSafe), set.New(set.ThreadSafe)
|
|
for i := 0; i < len(src); i++ {
|
|
s1.Add(src[i].String())
|
|
s2.Add(dest[i].String())
|
|
}
|
|
return s1.IsEqual(s2)
|
|
}
|
|
|
|
func CompareRules(src, dest SecRuleInfo, debug bool) (common, inAdds, outAdds, inDels, outDels SecurityRuleSet) {
|
|
srcInRules, srcOutRules := src.Split()
|
|
destInRules, destOutRules := dest.Split()
|
|
|
|
srcInRules, srcOutRules = src.AddDefaultRule(dest, srcInRules, srcOutRules, true)
|
|
destInRules, destOutRules = dest.AddDefaultRule(src, destInRules, destOutRules, false)
|
|
|
|
// AllowList 需要优先级从高到低排序
|
|
SortSecurityRule(srcInRules, false, src)
|
|
SortSecurityRule(srcOutRules, false, src)
|
|
|
|
SortSecurityRule(destInRules, false, dest)
|
|
SortSecurityRule(destOutRules, false, dest)
|
|
|
|
isInAllowSplit := srcInRules.CanBeSplitByProtocol() && destInRules.CanBeSplitByProtocol()
|
|
isOutAllowSplit := srcOutRules.CanBeSplitByProtocol() && destOutRules.CanBeSplitByProtocol()
|
|
|
|
srcInAllowList, srcInOk := srcInRules.AllowList()
|
|
srcOutAllowList, srcOutOk := srcOutRules.AllowList()
|
|
|
|
destInAllowList, destInOk := destInRules.AllowList()
|
|
destOutAllowList, destOutOk := destOutRules.AllowList()
|
|
inEquals, outEquals, inOk, outOk := isAllowListEqual(srcInAllowList, destInAllowList), isAllowListEqual(srcOutAllowList, destOutAllowList), srcInOk && destInOk, srcOutOk && destOutOk
|
|
|
|
if inEquals && outEquals && inOk && outOk {
|
|
common = dest.rules
|
|
return
|
|
}
|
|
|
|
if debug {
|
|
log.Debugf("====desc sort====")
|
|
log.Debugf("src in rules: ")
|
|
srcInRules.Debug()
|
|
log.Debugf("src out rules: ")
|
|
srcOutRules.Debug()
|
|
log.Debugf("dest in rules: ")
|
|
destInRules.Debug()
|
|
log.Debugf("dest out rules: ")
|
|
destOutRules.Debug()
|
|
log.Debugf("====desc sort end====")
|
|
|
|
log.Debugf("isInAllowSplit: %v isOutAllowSplit: %v", isInAllowSplit, isOutAllowSplit)
|
|
|
|
log.Debugf("AllowList:")
|
|
log.Debugf("In: src: %s dest: %s isEquals: %v", srcInAllowList.String(), destInAllowList.String(), inEquals)
|
|
log.Debugf("Out: src: %s dest: %s isEquals: %v", srcOutAllowList.String(), destOutAllowList.String(), outEquals)
|
|
}
|
|
|
|
var tryUseAllowList = func(defaultRule SecurityRule, allowList secrules.SecurityRuleSet, rules SecurityRuleSet, isOnlyAllowList bool, isOk bool) SecurityRuleSet {
|
|
if isOk && (len(allowList) < len(rules) || isOnlyAllowList) {
|
|
rules = SecurityRuleSet{}
|
|
for i := range allowList {
|
|
rule := SecurityRule{}
|
|
rule.SecurityRule = allowList[i]
|
|
rules = append(rules, rule)
|
|
}
|
|
|
|
if !utils.IsInStringArray(allowList.String(), []string{
|
|
"",
|
|
"in:allow any",
|
|
"out:allow any",
|
|
"in:deny any",
|
|
"out:deny any",
|
|
}) && strings.HasSuffix(defaultRule.SecurityRule.String(), "deny any") {
|
|
rules = append(rules, defaultRule)
|
|
}
|
|
}
|
|
return rules
|
|
}
|
|
|
|
srcInRules = tryUseAllowList(src.InDefaultRule, srcInAllowList, srcInRules, dest.IsOnlySupportAllowRules, inOk)
|
|
srcOutRules = tryUseAllowList(src.OutDefaultRule, srcOutAllowList, srcOutRules, dest.IsOnlySupportAllowRules, outOk)
|
|
|
|
if inEquals && inOk {
|
|
common = append(common, dest.in...)
|
|
srcInRules, destInRules = []SecurityRule{}, []SecurityRule{}
|
|
}
|
|
if outEquals && outOk {
|
|
common = append(common, dest.out...)
|
|
srcOutRules, destOutRules = []SecurityRule{}, []SecurityRule{}
|
|
}
|
|
|
|
// 默认从优先级低到高比较
|
|
SortSecurityRule(srcInRules, true, src)
|
|
SortSecurityRule(srcOutRules, true, src)
|
|
|
|
SortSecurityRule(destInRules, true, dest)
|
|
SortSecurityRule(destOutRules, true, dest)
|
|
|
|
if debug {
|
|
log.Debugf("====asc sort====")
|
|
log.Debugf("src in rules: ")
|
|
srcInRules.Debug()
|
|
log.Debugf("src out rules: ")
|
|
srcOutRules.Debug()
|
|
log.Debugf("dest in rules: ")
|
|
destInRules.Debug()
|
|
log.Debugf("dest out rules: ")
|
|
destOutRules.Debug()
|
|
log.Debugf("====asc sort end====")
|
|
}
|
|
|
|
var _compare = func(srcRules SecurityRuleSet, destRules SecurityRuleSet) (common, add, del SecurityRuleSet) {
|
|
i, j, priority := 0, 0, dest.MinPriority
|
|
if len(destRules) > 0 && (destRules[i].ExternalId != DEFAULT_DEST_RULE_ID) {
|
|
priority = destRules[0].Priority
|
|
}
|
|
for i < len(srcRules) || j < len(destRules) {
|
|
if i < len(srcRules) && j < len(destRules) {
|
|
destRuleStr := destRules[j].String()
|
|
srcRuleStr := srcRules[i].String()
|
|
if debug {
|
|
log.Debugf("compare src %s(%s) priority(%d) %s -> dest name(%s) %s(%s) priority(%d) %s\n",
|
|
srcRules[i].Id, srcRules[i].ExternalId, srcRules[i].Priority, srcRules[i].String(),
|
|
destRules[j].Name, destRules[j].ExternalId, destRules[j].Id, destRules[j].Priority, destRules[j].String())
|
|
}
|
|
cmp := strings.Compare(destRuleStr, srcRuleStr)
|
|
if cmp == 0 {
|
|
destRules[j].Id = srcRules[i].Id
|
|
common = append(common, destRules[j])
|
|
if destRules[j].ExternalId != DEFAULT_DEST_RULE_ID {
|
|
priority = destRules[j].Priority
|
|
}
|
|
i++
|
|
j++
|
|
} else if cmp < 0 {
|
|
del = append(del, destRules[j])
|
|
j++
|
|
} else {
|
|
offset := 0
|
|
if !dest.IsOnlySupportAllowRules && i-1 >= 0 && srcRules[i-1].Priority != srcRules[i].Priority {
|
|
priority, offset = dest.getOffest(priority)
|
|
}
|
|
if offset != 0 {
|
|
for r := range add {
|
|
add[r].addPriority(offset)
|
|
}
|
|
for r := range common {
|
|
common[r].addPriority(offset)
|
|
}
|
|
}
|
|
srcRules[i].Priority = priority
|
|
srcRules[i].minPriority, srcRules[i].maxPriority = dest.MinPriority, dest.MaxPriority
|
|
add = append(add, srcRules[i])
|
|
i++
|
|
}
|
|
} else if i >= len(srcRules) {
|
|
del = append(del, destRules[j])
|
|
j++
|
|
} else if j >= len(destRules) {
|
|
offset := 0
|
|
if !dest.IsOnlySupportAllowRules && i-1 >= 0 && srcRules[i-1].Priority != srcRules[i].Priority {
|
|
priority, offset = dest.getOffest(priority)
|
|
}
|
|
srcRules[i].Priority = priority
|
|
srcRules[i].minPriority, srcRules[i].maxPriority = dest.MinPriority, dest.MaxPriority
|
|
if offset != 0 {
|
|
for r := range add {
|
|
add[r].addPriority(offset)
|
|
}
|
|
for r := range common {
|
|
common[r].addPriority(offset)
|
|
}
|
|
}
|
|
add = append(add, srcRules[i])
|
|
i++
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
type rulePair struct {
|
|
srcRules SecurityRuleSet
|
|
destRules SecurityRuleSet
|
|
protocol string
|
|
}
|
|
|
|
var splitRules = func(src, dest SecurityRuleSet) []rulePair {
|
|
rules := map[string]rulePair{}
|
|
for _, r := range src {
|
|
pair, ok := rules[r.Protocol]
|
|
if !ok {
|
|
pair = rulePair{srcRules: SecurityRuleSet{}, destRules: SecurityRuleSet{}, protocol: r.Protocol}
|
|
}
|
|
pair.srcRules = append(pair.srcRules, r)
|
|
rules[r.Protocol] = pair
|
|
}
|
|
|
|
for _, r := range dest {
|
|
pair, ok := rules[r.Protocol]
|
|
if !ok {
|
|
pair = rulePair{srcRules: SecurityRuleSet{}, destRules: SecurityRuleSet{}, protocol: r.Protocol}
|
|
}
|
|
pair.destRules = append(pair.destRules, r)
|
|
rules[r.Protocol] = pair
|
|
}
|
|
|
|
ret := []rulePair{}
|
|
for _, r := range rules {
|
|
ret = append(ret, r)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
var compare = func(src, dest SecurityRuleSet) (common, added, dels SecurityRuleSet) {
|
|
pairs := splitRules(src, dest)
|
|
for _, r := range pairs {
|
|
_common, _add, _dels := _compare(r.srcRules, r.destRules)
|
|
common = append(common, _common...)
|
|
added = append(added, _add...)
|
|
dels = append(dels, _dels...)
|
|
}
|
|
return
|
|
}
|
|
|
|
var inCommon, outCommon SecurityRuleSet
|
|
if isInAllowSplit {
|
|
inCommon, inAdds, inDels = compare(srcInRules, destInRules)
|
|
} else {
|
|
inCommon, inAdds, inDels = _compare(srcInRules, destInRules)
|
|
}
|
|
if isOutAllowSplit {
|
|
outCommon, outAdds, outDels = compare(srcOutRules, destOutRules)
|
|
} else {
|
|
outCommon, outAdds, outDels = _compare(srcOutRules, destOutRules)
|
|
}
|
|
|
|
var handleDefaultRules = func(removed, added []SecurityRule, isOnlyAllowList bool) ([]SecurityRule, []SecurityRule, *SecurityRule) {
|
|
ret := []SecurityRule{}
|
|
var defaultRule *SecurityRule = nil
|
|
for i := range removed {
|
|
rule := removed[i]
|
|
if rule.ExternalId == DEFAULT_DEST_RULE_ID {
|
|
if debug {
|
|
log.Debugf("remove dest default rule: %s external id %s priority: %d", rule.String(), rule.ExternalId, rule.Priority)
|
|
}
|
|
if rule.Action == secrules.SecurityRuleDeny && isOnlyAllowList {
|
|
continue
|
|
}
|
|
switch rule.Action {
|
|
case secrules.SecurityRuleDeny:
|
|
rule.Action = secrules.SecurityRuleAllow
|
|
case secrules.SecurityRuleAllow:
|
|
rule.Action = secrules.SecurityRuleDeny
|
|
}
|
|
rule.Priority = dest.MinPriority
|
|
defaultRule = &rule
|
|
} else {
|
|
ret = append(ret, rule)
|
|
}
|
|
}
|
|
return ret, added, defaultRule
|
|
}
|
|
|
|
var inDefault *SecurityRule
|
|
var outDefault *SecurityRule
|
|
inDels, inAdds, inDefault = handleDefaultRules(inDels, inAdds, dest.IsOnlySupportAllowRules)
|
|
if inDefault != nil {
|
|
inAllow, _ := append(inAdds, inCommon...).AllowList()
|
|
if len(inAllow.String()) == 0 || (!strings.Contains(inAllow.String(), inDefault.String()) && !srcInAllowList.Equals(inAllow)) {
|
|
inAdds = append(inAdds, *inDefault)
|
|
}
|
|
}
|
|
outDels, outAdds, outDefault = handleDefaultRules(outDels, outAdds, dest.IsOnlySupportAllowRules)
|
|
|
|
if outDefault != nil {
|
|
outAllow, _ := append(outAdds, outCommon...).AllowList()
|
|
if len(outAllow.String()) == 0 || (!strings.Contains(outAllow.String(), outDefault.String()) && !srcOutAllowList.Equals(outAllow)) {
|
|
outAdds = append(outAdds, *outDefault)
|
|
}
|
|
}
|
|
_common, _, _ := handleDefaultRules(append(inCommon, outCommon...), []SecurityRule{}, dest.IsOnlySupportAllowRules)
|
|
common = append(common, _common...)
|
|
return
|
|
}
|
|
|
|
func SortUniqPriority(rules SecurityRuleSet) []SecurityRule {
|
|
sort.Sort(rules)
|
|
priMap := map[int]bool{}
|
|
for i := range rules {
|
|
for {
|
|
_, ok := priMap[rules[i].Priority]
|
|
if !ok {
|
|
priMap[rules[i].Priority] = true
|
|
break
|
|
}
|
|
rules[i].Priority = rules[i].Priority - 1
|
|
}
|
|
}
|
|
return rules
|
|
}
|