2020-09-13 20:37:28 +08:00
|
|
|
|
package installers
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
|
|
|
|
|
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
2020-10-26 21:14:56 +08:00
|
|
|
|
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
|
2020-09-13 20:37:28 +08:00
|
|
|
|
"github.com/iwind/TeaGo/logs"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var sharedQueue = NewQueue()
|
|
|
|
|
|
|
|
|
|
|
|
type Queue struct {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewQueue() *Queue {
|
|
|
|
|
|
return &Queue{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func SharedQueue() *Queue {
|
|
|
|
|
|
return sharedQueue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安装边缘节点流程控制
|
2020-10-28 12:35:36 +08:00
|
|
|
|
func (this *Queue) InstallNodeProcess(nodeId int64, isUpgrading bool) error {
|
2020-09-13 20:37:28 +08:00
|
|
|
|
installStatus := models.NewNodeInstallStatus()
|
|
|
|
|
|
installStatus.IsRunning = true
|
|
|
|
|
|
installStatus.UpdatedAt = time.Now().Unix()
|
|
|
|
|
|
|
2021-01-01 23:31:30 +08:00
|
|
|
|
err := models.SharedNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新时间
|
|
|
|
|
|
ticker := utils.NewTicker(3 * time.Second)
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
for ticker.Wait() {
|
|
|
|
|
|
installStatus.UpdatedAt = time.Now().Unix()
|
2021-01-01 23:31:30 +08:00
|
|
|
|
err := models.SharedNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
logs.Println("[INSTALL]" + err.Error())
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
ticker.Stop()
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
// 开始安装
|
2020-10-28 12:35:36 +08:00
|
|
|
|
err = this.InstallNode(nodeId, installStatus, isUpgrading)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 安装结束
|
|
|
|
|
|
installStatus.IsRunning = false
|
|
|
|
|
|
installStatus.IsFinished = true
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
installStatus.Error = err.Error()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
installStatus.IsOk = true
|
|
|
|
|
|
}
|
2021-01-01 23:31:30 +08:00
|
|
|
|
err = models.SharedNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 修改为已安装
|
|
|
|
|
|
if installStatus.IsOk {
|
2021-01-01 23:31:30 +08:00
|
|
|
|
err = models.SharedNodeDAO.UpdateNodeIsInstalled(nil, nodeId, true)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安装边缘节点
|
2020-10-28 12:35:36 +08:00
|
|
|
|
func (this *Queue) InstallNode(nodeId int64, installStatus *models.NodeInstallStatus, isUpgrading bool) error {
|
2021-01-01 23:31:30 +08:00
|
|
|
|
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if node == nil {
|
2020-10-26 21:14:56 +08:00
|
|
|
|
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
2020-09-13 20:37:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 登录信息
|
2021-01-01 23:31:30 +08:00
|
|
|
|
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeId)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if login == nil {
|
2020-10-26 21:14:56 +08:00
|
|
|
|
installStatus.ErrorCode = "EMPTY_LOGIN"
|
2020-09-13 20:37:28 +08:00
|
|
|
|
return errors.New("can not find node login information")
|
|
|
|
|
|
}
|
|
|
|
|
|
loginParams, err := login.DecodeSSHParams()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-10-26 21:14:56 +08:00
|
|
|
|
if len(loginParams.Host) == 0 {
|
|
|
|
|
|
installStatus.ErrorCode = "EMPTY_SSH_HOST"
|
|
|
|
|
|
return errors.New("ssh host should not be empty")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if loginParams.Port <= 0 {
|
|
|
|
|
|
installStatus.ErrorCode = "EMPTY_SSH_PORT"
|
|
|
|
|
|
return errors.New("ssh port is invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if loginParams.GrantId == 0 {
|
|
|
|
|
|
// 从集群中读取
|
2021-01-01 23:31:30 +08:00
|
|
|
|
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
2020-10-26 21:14:56 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if grantId == 0 {
|
|
|
|
|
|
installStatus.ErrorCode = "EMPTY_GRANT"
|
|
|
|
|
|
return errors.New("can not find node grant")
|
|
|
|
|
|
}
|
|
|
|
|
|
loginParams.GrantId = grantId
|
|
|
|
|
|
}
|
2021-01-01 23:31:30 +08:00
|
|
|
|
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if grant == nil {
|
2020-10-26 21:14:56 +08:00
|
|
|
|
installStatus.ErrorCode = "EMPTY_GRANT"
|
|
|
|
|
|
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
2020-09-13 20:37:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安装目录
|
|
|
|
|
|
installDir := node.InstallDir
|
|
|
|
|
|
if len(installDir) == 0 {
|
|
|
|
|
|
clusterId := node.ClusterId
|
2021-01-01 23:31:30 +08:00
|
|
|
|
cluster, err := models.SharedNodeClusterDAO.FindEnabledNodeCluster(nil, int64(clusterId))
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if cluster == nil {
|
|
|
|
|
|
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
installDir = cluster.InstallDir
|
|
|
|
|
|
if len(installDir) == 0 {
|
2020-10-26 21:14:56 +08:00
|
|
|
|
// 默认是 $登录用户/edge-node
|
|
|
|
|
|
installDir = "/" + grant.Username + "/edge-node"
|
2020-09-13 20:37:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// API终端
|
2021-01-01 23:31:30 +08:00
|
|
|
|
apiNodes, err := models.SharedAPINodeDAO.FindAllEnabledAndOnAPINodes(nil)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(apiNodes) == 0 {
|
|
|
|
|
|
return errors.New("no available api nodes")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
apiEndpoints := []string{}
|
|
|
|
|
|
for _, apiNode := range apiNodes {
|
2020-10-04 14:27:14 +08:00
|
|
|
|
addrConfigs, err := apiNode.DecodeAccessAddrs()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.New("decode api node access addresses failed: " + err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, addrConfig := range addrConfigs {
|
|
|
|
|
|
apiEndpoints = append(apiEndpoints, addrConfig.FullAddresses()...)
|
|
|
|
|
|
}
|
2020-09-13 20:37:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
params := &NodeParams{
|
2020-10-28 12:35:36 +08:00
|
|
|
|
Endpoints: apiEndpoints,
|
|
|
|
|
|
NodeId: node.UniqueId,
|
|
|
|
|
|
Secret: node.Secret,
|
|
|
|
|
|
IsUpgrading: isUpgrading,
|
2020-09-13 20:37:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
installer := &NodeInstaller{}
|
|
|
|
|
|
err = installer.Login(&Credentials{
|
|
|
|
|
|
Host: loginParams.Host,
|
|
|
|
|
|
Port: loginParams.Port,
|
|
|
|
|
|
Username: grant.Username,
|
|
|
|
|
|
Password: grant.Password,
|
|
|
|
|
|
PrivateKey: grant.PrivateKey,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2020-10-28 12:35:36 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
_ = installer.Close()
|
|
|
|
|
|
}()
|
2020-09-13 20:37:28 +08:00
|
|
|
|
|
2020-10-27 12:33:22 +08:00
|
|
|
|
err = installer.Install(installDir, params, installStatus)
|
2020-09-13 20:37:28 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
2020-10-27 12:33:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 启动边缘节点
|
|
|
|
|
|
func (this *Queue) StartNode(nodeId int64) error {
|
2021-01-01 23:31:30 +08:00
|
|
|
|
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if node == nil {
|
|
|
|
|
|
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 登录信息
|
2021-01-01 23:31:30 +08:00
|
|
|
|
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeId)
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if login == nil {
|
|
|
|
|
|
return errors.New("can not find node login information")
|
|
|
|
|
|
}
|
|
|
|
|
|
loginParams, err := login.DecodeSSHParams()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(loginParams.Host) == 0 {
|
|
|
|
|
|
return errors.New("ssh host should not be empty")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if loginParams.Port <= 0 {
|
|
|
|
|
|
return errors.New("ssh port is invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if loginParams.GrantId == 0 {
|
|
|
|
|
|
// 从集群中读取
|
2021-01-01 23:31:30 +08:00
|
|
|
|
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if grantId == 0 {
|
|
|
|
|
|
return errors.New("can not find node grant")
|
|
|
|
|
|
}
|
|
|
|
|
|
loginParams.GrantId = grantId
|
|
|
|
|
|
}
|
2021-01-01 23:31:30 +08:00
|
|
|
|
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if grant == nil {
|
|
|
|
|
|
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安装目录
|
|
|
|
|
|
installDir := node.InstallDir
|
|
|
|
|
|
if len(installDir) == 0 {
|
|
|
|
|
|
clusterId := node.ClusterId
|
2021-01-01 23:31:30 +08:00
|
|
|
|
cluster, err := models.SharedNodeClusterDAO.FindEnabledNodeCluster(nil, int64(clusterId))
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if cluster == nil {
|
|
|
|
|
|
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
installDir = cluster.InstallDir
|
|
|
|
|
|
if len(installDir) == 0 {
|
|
|
|
|
|
// 默认是 $登录用户/edge-node
|
|
|
|
|
|
installDir = "/" + grant.Username + "/edge-node"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
installer := &NodeInstaller{}
|
|
|
|
|
|
err = installer.Login(&Credentials{
|
|
|
|
|
|
Host: loginParams.Host,
|
|
|
|
|
|
Port: loginParams.Port,
|
|
|
|
|
|
Username: grant.Username,
|
|
|
|
|
|
Password: grant.Password,
|
|
|
|
|
|
PrivateKey: grant.PrivateKey,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2020-10-28 12:35:36 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
_ = installer.Close()
|
|
|
|
|
|
}()
|
2020-10-27 12:33:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查命令是否存在
|
|
|
|
|
|
exeFile := installDir + "/edge-node/bin/edge-node"
|
|
|
|
|
|
_, err = installer.client.Stat(exeFile)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.New("edge node is not installed correctly, can not find executable file: " + exeFile)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, stderr, err := installer.client.Exec(exeFile + " start")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.New("start failed: " + err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(stderr) > 0 {
|
|
|
|
|
|
return errors.New("start failed: " + stderr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 停止节点
|
|
|
|
|
|
func (this *Queue) StopNode(nodeId int64) error {
|
2021-01-01 23:31:30 +08:00
|
|
|
|
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if node == nil {
|
|
|
|
|
|
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 登录信息
|
2021-01-01 23:31:30 +08:00
|
|
|
|
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeId)
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if login == nil {
|
|
|
|
|
|
return errors.New("can not find node login information")
|
|
|
|
|
|
}
|
|
|
|
|
|
loginParams, err := login.DecodeSSHParams()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(loginParams.Host) == 0 {
|
|
|
|
|
|
return errors.New("ssh host should not be empty")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if loginParams.Port <= 0 {
|
|
|
|
|
|
return errors.New("ssh port is invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if loginParams.GrantId == 0 {
|
|
|
|
|
|
// 从集群中读取
|
2021-01-01 23:31:30 +08:00
|
|
|
|
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if grantId == 0 {
|
|
|
|
|
|
return errors.New("can not find node grant")
|
|
|
|
|
|
}
|
|
|
|
|
|
loginParams.GrantId = grantId
|
|
|
|
|
|
}
|
2021-01-01 23:31:30 +08:00
|
|
|
|
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if grant == nil {
|
|
|
|
|
|
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 安装目录
|
|
|
|
|
|
installDir := node.InstallDir
|
|
|
|
|
|
if len(installDir) == 0 {
|
|
|
|
|
|
clusterId := node.ClusterId
|
2021-01-01 23:31:30 +08:00
|
|
|
|
cluster, err := models.SharedNodeClusterDAO.FindEnabledNodeCluster(nil, int64(clusterId))
|
2020-10-27 12:33:22 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if cluster == nil {
|
|
|
|
|
|
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
|
|
|
|
|
}
|
|
|
|
|
|
installDir = cluster.InstallDir
|
|
|
|
|
|
if len(installDir) == 0 {
|
|
|
|
|
|
// 默认是 $登录用户/edge-node
|
|
|
|
|
|
installDir = "/" + grant.Username + "/edge-node"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
installer := &NodeInstaller{}
|
|
|
|
|
|
err = installer.Login(&Credentials{
|
|
|
|
|
|
Host: loginParams.Host,
|
|
|
|
|
|
Port: loginParams.Port,
|
|
|
|
|
|
Username: grant.Username,
|
|
|
|
|
|
Password: grant.Password,
|
|
|
|
|
|
PrivateKey: grant.PrivateKey,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2020-10-28 12:35:36 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
_ = installer.Close()
|
|
|
|
|
|
}()
|
2020-10-27 12:33:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查命令是否存在
|
|
|
|
|
|
exeFile := installDir + "/edge-node/bin/edge-node"
|
|
|
|
|
|
_, err = installer.client.Stat(exeFile)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.New("edge node is not installed correctly, can not find executable file: " + exeFile)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_, stderr, err := installer.client.Exec(exeFile + " stop")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return errors.New("start failed: " + err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(stderr) > 0 {
|
|
|
|
|
|
return errors.New("start failed: " + stderr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|