mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-21 00:24:07 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83896cdcc3 | ||
|
|
38914b967d | ||
|
|
7695eb905c | ||
|
|
562aefda85 | ||
|
|
0143d271e1 | ||
|
|
de2b95cab7 | ||
|
|
fc5a78c7e3 | ||
|
|
3986dbc434 | ||
|
|
192e03fcd7 | ||
|
|
44db018039 | ||
|
|
6751d1d3af | ||
|
|
5ea100865d | ||
|
|
987b641bd0 | ||
|
|
77e56db095 | ||
|
|
b7629422f7 | ||
|
|
589daf5063 | ||
|
|
a066301ae2 | ||
|
|
7d07b4aa5f | ||
|
|
b96f225274 | ||
|
|
40c8b54cba | ||
|
|
93879980f5 | ||
|
|
5bb13e48b7 | ||
|
|
64fcdea2b0 | ||
|
|
57fa6361fc | ||
|
|
732159137b | ||
|
|
d2ec564ccc | ||
|
|
a4323eab43 | ||
|
|
7335875ef1 | ||
|
|
8d98218f06 | ||
|
|
af274d7ad2 | ||
|
|
25baa6f509 |
@@ -34,6 +34,7 @@ import (
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/monitor"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/notifyv2"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/servicetree"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/suggestion"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/yunionconf"
|
||||
)
|
||||
|
||||
|
||||
@@ -407,12 +407,14 @@ func init() {
|
||||
})
|
||||
|
||||
type HostAddNetIfOptions struct {
|
||||
ID string `help:"ID or Name of host"`
|
||||
WIRE string `help:"ID or Name of wire to attach"`
|
||||
MAC string `help:"Mac address of NIC"`
|
||||
INDEX int64 `help:"nic index"`
|
||||
Type string `help:"Nic type" choices:"admin|ipmi"`
|
||||
IpAddr string `help:"IP address"`
|
||||
ID string `help:"ID or Name of host"`
|
||||
WIRE string `help:"ID or Name of wire to attach"`
|
||||
MAC string `help:"Mac address of NIC"`
|
||||
INDEX int64 `help:"nic index"`
|
||||
Type string `help:"Nic type" choices:"admin|ipmi"`
|
||||
IpAddr string `help:"IP address"`
|
||||
Bridge string `help:"Bridge of hostwire"`
|
||||
Interface string `help:"Interface name, eg:eth0, en0"`
|
||||
}
|
||||
R(&HostAddNetIfOptions{}, "host-add-netif", "Host add a NIC", func(s *mcclient.ClientSession, args *HostAddNetIfOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
@@ -426,6 +428,12 @@ func init() {
|
||||
if len(args.IpAddr) > 0 {
|
||||
params.Add(jsonutils.NewString(args.IpAddr), "ip_addr")
|
||||
}
|
||||
if len(args.Bridge) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Bridge), "bridge")
|
||||
}
|
||||
if len(args.Interface) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Interface), "interface")
|
||||
}
|
||||
result, err := modules.Hosts.PerformAction(s, args.ID, "add-netif", params)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,4 +10,5 @@ func init() {
|
||||
cmd := shell.NewResourceCmd(modules.AlertRecordManager)
|
||||
cmd.List(new(options.AlertRecordListOptions))
|
||||
cmd.Show(new(options.AlertRecordShowOptions))
|
||||
cmd.Get("", new(options.AlertRecordTotalOptions))
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// 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 monitor
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
|
||||
options "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
|
||||
)
|
||||
|
||||
func init() {
|
||||
aN := cmdN("suggestsysalert")
|
||||
R(&options.SuggestSysAlertListOptions{}, aN("list"), "List all suggestsysrules",
|
||||
func(s *mcclient.ClientSession, args *options.SuggestSysAlertListOptions) error {
|
||||
params, err := args.Params()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args.Type) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Type), "type")
|
||||
}
|
||||
ret, err := monitor.SuggestSysAlertManager.List(s, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printList(ret, monitor.SuggestSysAlertManager.GetColumns(s))
|
||||
return nil
|
||||
})
|
||||
|
||||
R(&options.SSuggestAlertShowOptions{}, aN("show"), "Show details of a alert rule",
|
||||
func(s *mcclient.ClientSession, args *options.SSuggestAlertShowOptions) error {
|
||||
ret, err := monitor.SuggestSysAlertManager.Get(s, args.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(ret)
|
||||
return nil
|
||||
})
|
||||
|
||||
R(&options.SuggestAlertIgnoreOptions{}, aN("ignore"), "Ignore alert result",
|
||||
func(s *mcclient.ClientSession, args *options.SuggestAlertIgnoreOptions) error {
|
||||
params, err := args.Params()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret, err := monitor.SuggestSysAlertManager.PerformAction(s, args.ID, "ignore", params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printObject(ret)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
12
cmd/climc/shell/suggestion/analysispredict.go
Normal file
12
cmd/climc/shell/suggestion/analysispredict.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package suggestion
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/cmd/climc/shell"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||||
options "yunion.io/x/onecloud/pkg/mcclient/options/suggestion"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd := shell.NewResourceCmd(modules.AnalysisPredictManager)
|
||||
cmd.Get("", new(options.AnalysisPredictConfigOptions))
|
||||
}
|
||||
28
cmd/climc/shell/suggestion/suggestsysalert.go
Normal file
28
cmd/climc/shell/suggestion/suggestsysalert.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 suggestion
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/cmd/climc/shell"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules/monitor"
|
||||
options "yunion.io/x/onecloud/pkg/mcclient/options/monitor"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd := shell.NewResourceCmd(monitor.SuggestSysAlertManager)
|
||||
cmd.List(new(options.SuggestSysAlertListOptions))
|
||||
cmd.Show(new(options.SSuggestAlertShowOptions))
|
||||
cmd.Perform("ignore", new(options.SuggestAlertIgnoreOptions))
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package monitor
|
||||
package suggestion
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/cmd/climc/shell"
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
type CloudeventListInput struct {
|
||||
apis.ModelBaseListInput
|
||||
apis.ProjectizedResourceListInput
|
||||
apis.DomainizedResourceListInput
|
||||
|
||||
compute.CloudenvResourceListInput
|
||||
|
||||
@@ -47,6 +47,6 @@ type CloudeventListInput struct {
|
||||
|
||||
type CloudeventDetails struct {
|
||||
apis.ModelBaseDetails
|
||||
apis.ProjectizedResourceInfo
|
||||
apis.DomainizedResourceInfo
|
||||
SCloudevent
|
||||
}
|
||||
|
||||
@@ -294,6 +294,8 @@ type DBInstanceDetails struct {
|
||||
|
||||
SDBInstance
|
||||
|
||||
Secgroups []apis.StandaloneShortDesc `json:"secgroups"`
|
||||
|
||||
// 安全组名称
|
||||
// example: Default
|
||||
Secgroup string `json:"secgroup"`
|
||||
|
||||
@@ -23,6 +23,9 @@ import (
|
||||
type ScheduledTaskDetails struct {
|
||||
apis.VirtualResourceDetails
|
||||
SScheduledTask
|
||||
|
||||
// 描述
|
||||
TimerDesc string `json:"timer_desc"`
|
||||
// 定时方式触发
|
||||
Timer TimerDetails `json:"timer"`
|
||||
// 周期方式触发
|
||||
|
||||
@@ -2068,7 +2068,6 @@ type SScheduledTask struct {
|
||||
apis.SEnabledResourceBase
|
||||
ScheduledType string `json:"scheduled_type"`
|
||||
STimer
|
||||
TimerDesc string `json:"timer_desc"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Operation string `json:"operation"`
|
||||
LabelType string `json:"label_type"`
|
||||
|
||||
@@ -205,6 +205,8 @@ type PerformStatusInput struct {
|
||||
// 更改的目标状态值
|
||||
// required:true
|
||||
Status string `json:"status"`
|
||||
// swagger:ignore
|
||||
BlockJobsCount int `json:"block_jobs_count"`
|
||||
|
||||
// 更改状态的原因描述
|
||||
// required:false
|
||||
|
||||
@@ -47,8 +47,9 @@ const (
|
||||
ACT_BACKUP_START = "backup_start"
|
||||
ACT_BACKUP_START_FAILED = "backup_start_fail"
|
||||
|
||||
ACT_FREEZE = "freeze"
|
||||
ACT_UNFREEZE = "unfreeze"
|
||||
ACT_FREEZE = "freeze"
|
||||
ACT_FREEZE_FAIL = "freeze_fail"
|
||||
ACT_UNFREEZE = "unfreeze"
|
||||
|
||||
ACT_RESTARING = "restarting"
|
||||
ACT_RESTART_FAIL = "restart_fail"
|
||||
|
||||
@@ -51,6 +51,8 @@ func isObjectRbacAllowed(model IModel, userCred mcclient.TokenCredential, action
|
||||
case rbacutils.ScopeUser:
|
||||
if ownerId != nil && objOwnerId != nil && (ownerId.GetUserId() == objOwnerId.GetUserId() || objOwnerId.GetUserId() == "" || (model.IsSharable(ownerId) && action == policy.PolicyActionGet)) {
|
||||
requireScope = rbacutils.ScopeUser
|
||||
} else if ownerId != nil && objOwnerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
|
||||
requireScope = rbacutils.ScopeDomain
|
||||
} else {
|
||||
requireScope = rbacutils.ScopeSystem
|
||||
}
|
||||
@@ -103,6 +105,8 @@ func isClassRbacAllowed(manager IModelManager, userCred mcclient.TokenCredential
|
||||
case rbacutils.ScopeUser:
|
||||
if ownerId != nil && ownerId.GetUserId() == objOwnerId.GetUserId() {
|
||||
requireScope = rbacutils.ScopeUser
|
||||
} else if ownerId != nil && ownerId.GetProjectDomainId() == objOwnerId.GetProjectDomainId() {
|
||||
requireScope = rbacutils.ScopeDomain
|
||||
} else {
|
||||
requireScope = rbacutils.ScopeSystem
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
|
||||
type SCloudeventManager struct {
|
||||
db.SModelBaseManager
|
||||
db.SProjectizedResourceBaseManager
|
||||
db.SDomainizedResourceBaseManager
|
||||
}
|
||||
|
||||
var CloudeventManager *SCloudeventManager
|
||||
@@ -57,7 +57,7 @@ func init() {
|
||||
|
||||
type SCloudevent struct {
|
||||
db.SModelBase
|
||||
db.SProjectizedResourceBase
|
||||
db.SDomainizedResourceBase
|
||||
|
||||
EventId int64 `primary:"true" auto_increment:"true" list:"user"`
|
||||
Name string `width:"128" charset:"utf8" nullable:"false" index:"true" list:"user"`
|
||||
@@ -76,6 +76,10 @@ type SCloudevent struct {
|
||||
Brand string `width:"64" charset:"ascii" list:"domain"`
|
||||
}
|
||||
|
||||
func (self *SCloudeventManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
|
||||
return db.IsDomainAllowList(userCred, self)
|
||||
}
|
||||
|
||||
func (self *SCloudeventManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
|
||||
return false
|
||||
}
|
||||
@@ -88,6 +92,10 @@ func (self *SCloudevent) AllowUpdateItem(ctx context.Context, userCred mcclient.
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *SCloudevent) AllowGetDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
|
||||
return db.IsDomainAllowGet(userCred, self)
|
||||
}
|
||||
|
||||
// 云平台操作日志列表
|
||||
func (manager *SCloudeventManager) ListItemFilter(
|
||||
ctx context.Context,
|
||||
@@ -97,7 +105,11 @@ func (manager *SCloudeventManager) ListItemFilter(
|
||||
) (*sqlchemy.SQuery, error) {
|
||||
q, err := manager.SModelBaseManager.ListItemFilter(ctx, q, userCred, input.ModelBaseListInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter")
|
||||
return nil, errors.Wrap(err, "SModelBaseManager.ListItemFilter")
|
||||
}
|
||||
q, err = manager.SDomainizedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainizedResourceListInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "SDomainizedResourceBaseManager.ListItemFilter")
|
||||
}
|
||||
|
||||
if len(input.Providers) > 0 {
|
||||
@@ -153,37 +165,45 @@ func (manager *SCloudeventManager) FetchCustomizeColumns(
|
||||
) []api.CloudeventDetails {
|
||||
rows := make([]api.CloudeventDetails, len(objs))
|
||||
base := manager.SModelBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
|
||||
projRows := manager.SProjectizedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
|
||||
domainRows := manager.SDomainizedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
|
||||
for i := range rows {
|
||||
rows[i].ModelBaseDetails = base[i]
|
||||
rows[i].ProjectizedResourceInfo = projRows[i]
|
||||
rows[i].DomainizedResourceInfo = domainRows[i]
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (self *SCloudevent) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
|
||||
return self.SModelBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) NamespaceScope() rbacutils.TRbacScope {
|
||||
return rbacutils.ScopeDomain
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) ResourceScope() rbacutils.TRbacScope {
|
||||
return rbacutils.ScopeProject
|
||||
return rbacutils.ScopeDomain
|
||||
}
|
||||
|
||||
func (self *SCloudevent) GetOwnerId() mcclient.IIdentityProvider {
|
||||
owner := db.SOwnerId{DomainId: self.DomainId, ProjectId: self.ProjectId}
|
||||
owner := db.SOwnerId{DomainId: self.DomainId}
|
||||
return &owner
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider, scope rbacutils.TRbacScope) *sqlchemy.SQuery {
|
||||
return manager.SProjectizedResourceBaseManager.FilterByOwner(q, owner, scope)
|
||||
return manager.SDomainizedResourceBaseManager.FilterByOwner(q, owner, scope)
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
|
||||
return manager.SProjectizedResourceBaseManager.FetchOwnerId(ctx, data)
|
||||
return manager.SDomainizedResourceBaseManager.FetchOwnerId(ctx, data)
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) ListItemExportKeys(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, keys stringutils2.SSortedStrings) (*sqlchemy.SQuery, error) {
|
||||
return manager.SProjectizedResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys)
|
||||
return manager.SDomainizedResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys)
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
|
||||
return manager.SProjectizedResourceBaseManager.QueryDistinctExtraField(q, field)
|
||||
return manager.SDomainizedResourceBaseManager.QueryDistinctExtraField(q, field)
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) OrderByExtraFields(
|
||||
@@ -192,7 +212,7 @@ func (manager *SCloudeventManager) OrderByExtraFields(
|
||||
userCred mcclient.TokenCredential,
|
||||
query api.CloudeventListInput,
|
||||
) (*sqlchemy.SQuery, error) {
|
||||
return manager.SProjectizedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.ProjectizedResourceListInput)
|
||||
return manager.SDomainizedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainizedResourceListInput)
|
||||
}
|
||||
|
||||
func (manager *SCloudeventManager) SyncCloudevent(ctx context.Context, userCred mcclient.TokenCredential, cloudprovider *SCloudprovider, iEvents []cloudprovider.ICloudEvent) int {
|
||||
@@ -213,7 +233,6 @@ func (manager *SCloudeventManager) SyncCloudevent(ctx context.Context, userCred
|
||||
CloudproviderId: cloudprovider.Id,
|
||||
}
|
||||
event.DomainId = cloudprovider.DomainId
|
||||
event.ProjectId = cloudprovider.ProjectId
|
||||
if len(event.Brand) == 0 {
|
||||
event.Brand = event.Provider
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import (
|
||||
|
||||
var (
|
||||
cloudeventSystemResources = []string{}
|
||||
cloudeventDomainResources = []string{}
|
||||
cloudeventUserResources = []string{}
|
||||
cloudeventDomainResources = []string{
|
||||
"cloudevent",
|
||||
}
|
||||
cloudeventUserResources = []string{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/quotas"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/validators"
|
||||
"yunion.io/x/onecloud/pkg/cloudprovider"
|
||||
"yunion.io/x/onecloud/pkg/compute/options"
|
||||
@@ -537,6 +538,7 @@ func (manager *SDBInstanceManager) FetchCustomizeColumns(
|
||||
manRows := manager.SManagedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
|
||||
regRows := manager.SCloudregionResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
|
||||
|
||||
rdsIds := make([]string, len(rows))
|
||||
vpcIds := make([]string, len(rows))
|
||||
zone1Ids := make([]string, len(rows))
|
||||
zone2Ids := make([]string, len(rows))
|
||||
@@ -548,6 +550,7 @@ func (manager *SDBInstanceManager) FetchCustomizeColumns(
|
||||
CloudregionResourceInfo: regRows[i],
|
||||
}
|
||||
instance := objs[i].(*SDBInstance)
|
||||
rdsIds[i] = instance.Id
|
||||
vpcIds[i] = instance.VpcId
|
||||
zone1Ids[i] = instance.Zone1
|
||||
zone2Ids[i] = instance.Zone2
|
||||
@@ -569,6 +572,42 @@ func (manager *SDBInstanceManager) FetchCustomizeColumns(
|
||||
}
|
||||
}
|
||||
|
||||
q := SecurityGroupManager.Query()
|
||||
ownerId, queryScope, err := db.FetchCheckQueryOwnerScope(ctx, userCred, query, SecurityGroupManager, policy.PolicyActionList, true)
|
||||
if err != nil {
|
||||
log.Errorf("FetchCheckQueryOwnerScope error: %v", err)
|
||||
return rows
|
||||
}
|
||||
secgroups := SecurityGroupManager.FilterByOwner(q, ownerId, queryScope).SubQuery()
|
||||
rdssecgroups := DBInstanceSecgroupManager.Query().SubQuery()
|
||||
|
||||
secQ := rdssecgroups.Query(rdssecgroups.Field("dbinstance_id"), rdssecgroups.Field("secgroup_id"), secgroups.Field("name").Label("secgroup_name")).Join(secgroups, sqlchemy.Equals(rdssecgroups.Field("secgroup_id"), secgroups.Field("id"))).Filter(sqlchemy.In(rdssecgroups.Field("dbinstance_id"), rdsIds))
|
||||
|
||||
type sRdsSecgroupInfo struct {
|
||||
DBInstanceId string `json:"dbinstance_id"`
|
||||
SecgroupName string
|
||||
SecgroupId string
|
||||
}
|
||||
rsgs := []sRdsSecgroupInfo{}
|
||||
err = secQ.All(&rsgs)
|
||||
if err != nil {
|
||||
log.Errorf("secQ.All error: %v", err)
|
||||
return rows
|
||||
}
|
||||
|
||||
ret := make(map[string][]apis.StandaloneShortDesc)
|
||||
for i := range rsgs {
|
||||
rsg, ok := ret[rsgs[i].DBInstanceId]
|
||||
if !ok {
|
||||
rsg = make([]apis.StandaloneShortDesc, 0)
|
||||
}
|
||||
rsg = append(rsg, apis.StandaloneShortDesc{
|
||||
Id: rsgs[i].SecgroupId,
|
||||
Name: rsgs[i].SecgroupName,
|
||||
})
|
||||
ret[rsgs[i].DBInstanceId] = rsg
|
||||
}
|
||||
|
||||
zone1, err := db.FetchIdNameMap2(ZoneManager, zone1Ids)
|
||||
if err != nil {
|
||||
return rows
|
||||
@@ -588,6 +627,7 @@ func (manager *SDBInstanceManager) FetchCustomizeColumns(
|
||||
rows[i].Zone1Name = zone1[zone1Ids[i]]
|
||||
rows[i].Zone2Name = zone2[zone2Ids[i]]
|
||||
rows[i].Zone3Name = zone3[zone3Ids[i]]
|
||||
rows[i].Secgroups, _ = ret[rdsIds[i]]
|
||||
}
|
||||
|
||||
return rows
|
||||
|
||||
@@ -805,6 +805,16 @@ func (self *SGuest) AllowPerformStart(ctx context.Context,
|
||||
func (self *SGuest) PerformStart(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject,
|
||||
data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
if utils.IsInStringArray(self.Status, []string{api.VM_READY, api.VM_START_FAILED, api.VM_SAVE_DISK_FAILED, api.VM_SUSPEND}) {
|
||||
if !self.guestDisksStorageTypeIsShared() {
|
||||
host := self.GetHost()
|
||||
guestsMem, err := host.GetNotReadyGuestsMemorySize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if float32(guestsMem+self.VmemSize) > host.GetVirtualMemorySize() {
|
||||
return nil, httperrors.NewInsufficientResourceError("host virtual memory not enough")
|
||||
}
|
||||
}
|
||||
if self.isAllDisksReady() {
|
||||
var kwargs *jsonutils.JSONDict
|
||||
if data != nil {
|
||||
@@ -2726,7 +2736,7 @@ func (self *SGuest) PerformStatus(ctx context.Context, userCred mcclient.TokenCr
|
||||
}
|
||||
|
||||
status := input.Status
|
||||
if len(self.BackupHostId) > 0 && status == api.VM_RUNNING {
|
||||
if len(self.BackupHostId) > 0 && status == api.VM_RUNNING && input.BlockJobsCount > 0 {
|
||||
self.SetMetadata(ctx, api.MIRROR_JOB, api.MIRROR_JOB_READY, userCred)
|
||||
} else if ispId := self.GetMetadata(api.BASE_INSTANCE_SNAPSHOT_ID, userCred); len(ispId) > 0 {
|
||||
ispM, err := InstanceSnapshotManager.FetchById(ispId)
|
||||
@@ -2763,6 +2773,27 @@ func (self *SGuest) PerformStop(ctx context.Context, userCred mcclient.TokenCred
|
||||
return nil, httperrors.NewInvalidStatusError("Cannot stop server in status %s", self.Status)
|
||||
}
|
||||
|
||||
func (self *SGuest) PerformFreeze(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformFreezeInput) (jsonutils.JSONObject, error) {
|
||||
if self.Freezed {
|
||||
return nil, httperrors.NewBadRequestError("virtual resource already freezed")
|
||||
}
|
||||
if utils.IsInStringArray(self.Status, []string{api.VM_RUNNING, api.VM_STOP_FAILED}) {
|
||||
return nil, self.StartGuestStopAndFreezeTask(ctx, userCred)
|
||||
} else {
|
||||
return self.SVirtualResourceBase.PerformFreeze(ctx, userCred, query, input)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SGuest) StartGuestStopAndFreezeTask(ctx context.Context, userCred mcclient.TokenCredential) error {
|
||||
self.SetStatus(userCred, api.VM_START_STOP, "")
|
||||
task, err := taskman.TaskManager.NewTask(ctx, "GuestStopAndFreezeTask", self, userCred, nil, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.ScheduleRun(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SGuest) AllowPerformRestart(ctx context.Context,
|
||||
userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject,
|
||||
|
||||
@@ -4784,7 +4784,7 @@ func (self *SGuestManager) switchBackupGuests(ctx context.Context, userCred mccl
|
||||
log.Errorf("ReconcileBackupGuests failed fetch guests %s", err)
|
||||
return
|
||||
}
|
||||
log.Infof("Guests count %d need reconcile with switch bakcup", len(guests))
|
||||
log.Debugf("Guests count %d need reconcile with switch backup", len(guests))
|
||||
for i := 0; i < len(guests); i++ {
|
||||
val := guests[i].GetMetadataJson("switch_backup", userCred)
|
||||
t, err := val.GetTime()
|
||||
|
||||
@@ -1497,6 +1497,23 @@ func (self *SHost) GetRunningGuestCount() (int, error) {
|
||||
return q.CountWithError()
|
||||
}
|
||||
|
||||
func (self *SHost) GetNotReadyGuestsMemorySize() (int, error) {
|
||||
guests := GuestManager.Query().SubQuery()
|
||||
q := guests.Query(sqlchemy.COUNT("guest_count"),
|
||||
sqlchemy.SUM("guest_vcpu_count", guests.Field("vcpu_count")),
|
||||
sqlchemy.SUM("guest_vmem_size", guests.Field("vmem_size")))
|
||||
cond := sqlchemy.OR(sqlchemy.Equals(q.Field("host_id"), self.Id),
|
||||
sqlchemy.Equals(q.Field("backup_host_id"), self.Id))
|
||||
q = q.Filter(cond)
|
||||
q = q.NotEquals("status", api.VM_READY)
|
||||
stat := SHostGuestResourceUsage{}
|
||||
err := q.First(&stat)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return stat.GuestVmemSize, nil
|
||||
}
|
||||
|
||||
func (self *SHost) GetRunningGuestMemorySize() int {
|
||||
res := self.getGuestsResource(api.VM_RUNNING)
|
||||
if res != nil {
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
@@ -205,17 +203,7 @@ func (st *SScalingTimer) TriggerId() string {
|
||||
var cstSh, _ = time.LoadLocation("Asia/Shanghai")
|
||||
|
||||
func (st *SScalingTimer) TriggerDescription() string {
|
||||
var detail string
|
||||
switch st.Type {
|
||||
case api.TIMER_TYPE_ONCE:
|
||||
detail = st.EndTime.In(cstSh).Format("2006-01-02 15:04:05")
|
||||
case api.TIMER_TYPE_DAY:
|
||||
detail = fmt.Sprintf("%d:%d every day", st.Hour, st.Minute)
|
||||
case api.TIMER_TYPE_WEEK:
|
||||
detail = st.WeekDaysDesc()
|
||||
case api.TIMER_TYPE_MONTH:
|
||||
detail = st.MonthDaysDesc()
|
||||
}
|
||||
detail := st.descEnglish()
|
||||
name := st.ScalingPolicyId
|
||||
sp, _ := st.ScalingPolicy()
|
||||
if sp != nil {
|
||||
@@ -336,11 +324,14 @@ var indicatorMap = map[string]sTableField{
|
||||
api.INDICATOR_FLOW_OUT: {"vm_netio", "bps_sent"},
|
||||
}
|
||||
|
||||
var alertConfigUsedBy = "scaling_group"
|
||||
|
||||
func (sa *SScalingAlarm) generateAlertConfig(sp *SScalingPolicy) (*monitor.AlertConfig, error) {
|
||||
config, err := monitor.NewAlertConfig(fmt.Sprintf("sp-%s", sp.Id), fmt.Sprintf("%ds", sa.Cycle), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.UsedBy = alertConfigUsedBy
|
||||
cond := config.Condition("telegraf", indicatorMap[sa.Indicator].Table).Avg()
|
||||
log.Debugf("alarm: %#v", sa)
|
||||
|
||||
@@ -442,61 +433,3 @@ var units = map[string]string{
|
||||
api.INDICATOR_FLOW_INTO: "KB/s",
|
||||
api.INDICATOR_FLOW_OUT: "KB/s",
|
||||
}
|
||||
|
||||
var weekDays = []string{"", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|
||||
|
||||
func (st *SScalingTimer) WeekDaysDesc() string {
|
||||
if st.WeekDays == 0 {
|
||||
return ""
|
||||
}
|
||||
var desc strings.Builder
|
||||
wds := st.GetWeekDays()
|
||||
i := 0
|
||||
desc.WriteString(fmt.Sprintf("%d:%d every %s", st.Hour, st.Minute, weekDays[wds[i]]))
|
||||
for i++; i < len(wds)-1; i++ {
|
||||
desc.WriteString(", ")
|
||||
desc.WriteString(weekDays[wds[i]])
|
||||
}
|
||||
if i == len(wds)-1 {
|
||||
desc.WriteString(" and ")
|
||||
desc.WriteString(weekDays[wds[i]])
|
||||
}
|
||||
return desc.String()
|
||||
}
|
||||
|
||||
func (st *SScalingTimer) MonthDaysDesc() string {
|
||||
if st.MonthDays == 0 {
|
||||
return ""
|
||||
}
|
||||
var desc strings.Builder
|
||||
mds := st.GetMonthDays()
|
||||
i := 0
|
||||
desc.WriteString(fmt.Sprintf("%d:%d on the %d%s", st.Hour, st.Minute, mds[i], dateSuffix(mds[i])))
|
||||
for i++; i < len(mds)-1; i++ {
|
||||
desc.WriteString(", ")
|
||||
desc.WriteString(strconv.Itoa(mds[i]))
|
||||
desc.WriteString(dateSuffix(mds[i]))
|
||||
}
|
||||
if i == len(mds)-1 {
|
||||
desc.WriteString(" and ")
|
||||
desc.WriteString(strconv.Itoa(mds[i]))
|
||||
desc.WriteString(dateSuffix(mds[i]))
|
||||
}
|
||||
desc.WriteString(" of each month")
|
||||
return desc.String()
|
||||
}
|
||||
|
||||
func dateSuffix(date int) string {
|
||||
var ret string
|
||||
switch date {
|
||||
case 1:
|
||||
ret = "st"
|
||||
case 2:
|
||||
ret = "nd"
|
||||
case 3:
|
||||
ret = "rd"
|
||||
default:
|
||||
ret = "th"
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ type SScheduledTask struct {
|
||||
|
||||
STimer
|
||||
|
||||
TimerDesc string `width:"128" charset:"utf8" list:"user" get:"user"`
|
||||
ResourceType string `width:"32" charset:"ascii" create:"required" list:"user" get:"user"`
|
||||
Operation string `width:"32" charset:"ascii" create:"required" list:"user" get:"user"`
|
||||
LabelType string `width:"4" charset:"ascii" create:"required" list:"user" get:"user"`
|
||||
@@ -139,6 +138,7 @@ func (st *SScheduledTask) getMoreDetails(ctx context.Context, userCred mcclient.
|
||||
case api.ST_TYPE_CYCLE:
|
||||
out.CycleTimer = st.STimer.CycleTimerDetails()
|
||||
}
|
||||
out.TimerDesc = st.Description(ctx)
|
||||
// fill label
|
||||
stLabels, err := st.STLabels()
|
||||
if err != nil {
|
||||
@@ -184,36 +184,6 @@ func (stm *SScheduledTaskManager) ValidateCreateData(ctx context.Context, userCr
|
||||
return input, nil
|
||||
}
|
||||
|
||||
var wdsCN = []string{"", "一", "二", "三", "四", "五", "六", "日"}
|
||||
var zone = time.FixedZone("GMT", 8*3600)
|
||||
|
||||
func (st *SScheduledTask) TimerDescription() string {
|
||||
format := "2006-01-02 15:04:05"
|
||||
var prefix string
|
||||
timer := st.STimer
|
||||
switch timer.Type {
|
||||
case api.TIMER_TYPE_ONCE:
|
||||
return fmt.Sprintf("单次 %s触发", timer.StartTime.In(zone).Format(format))
|
||||
case api.TIMER_TYPE_DAY:
|
||||
prefix = "每天"
|
||||
case api.TIMER_TYPE_WEEK:
|
||||
wds := timer.GetWeekDays()
|
||||
weekDays := make([]string, len(wds))
|
||||
for i := range wds {
|
||||
weekDays[i] = fmt.Sprintf("星期%s", wdsCN[wds[i]])
|
||||
}
|
||||
prefix = fmt.Sprintf("每周 【%s】", strings.Join(weekDays, "|"))
|
||||
case api.TIMER_TYPE_MONTH:
|
||||
mns := timer.GetMonthDays()
|
||||
monthDays := make([]string, len(mns))
|
||||
for i := range mns {
|
||||
monthDays[i] = fmt.Sprintf("%d号", mns[i])
|
||||
}
|
||||
prefix = fmt.Sprintf("每月 【%s】", strings.Join(monthDays, "|"))
|
||||
}
|
||||
return fmt.Sprintf("%s %02d:%02d触发 有效时间为%s至%s", prefix, timer.Hour, timer.Minute, timer.StartTime.In(zone).Format(format), timer.EndTime.In(zone).Format(format))
|
||||
}
|
||||
|
||||
func (st *SScheduledTask) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject, input apis.PerformEnableInput) bool {
|
||||
return true
|
||||
@@ -288,7 +258,7 @@ func (st *SScheduledTask) PostCreate(ctx context.Context, userCred mcclient.Toke
|
||||
st.Update(time.Time{})
|
||||
st.Status = api.ST_STATUS_READY
|
||||
st.Enabled = tristate.True
|
||||
st.TimerDesc = st.TimerDescription()
|
||||
// st.TimerDesc = st.Description(ctx)
|
||||
err = st.GetModelManager().TableSpec().InsertOrUpdate(ctx, st)
|
||||
if err != nil {
|
||||
createFailed("update itself")
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/log"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
"yunion.io/x/onecloud/pkg/i18n"
|
||||
"yunion.io/x/onecloud/pkg/util/bitmap"
|
||||
)
|
||||
|
||||
@@ -159,6 +163,134 @@ func checkTimerCreateInput(in api.TimerCreateInput) (api.TimerCreateInput, error
|
||||
return in, nil
|
||||
}
|
||||
|
||||
var (
|
||||
timerDescTable = i18n.Table{}
|
||||
TIMERLANG = "timerLang"
|
||||
)
|
||||
|
||||
func init() {
|
||||
timerDescTable.Set("timerLang", i18n.NewTableEntry().EN("en").CN("cn"))
|
||||
}
|
||||
|
||||
func (st *STimer) Description(ctx context.Context) string {
|
||||
lang := timerDescTable.Lookup(ctx, TIMERLANG)
|
||||
switch lang {
|
||||
case "en":
|
||||
return st.descEnglish()
|
||||
case "cn":
|
||||
return st.descChinese()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
wdsCN = []string{"", "一", "二", "三", "四", "五", "六", "日"}
|
||||
wdsEN = []string{"", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|
||||
zone = time.Now().Local().Location()
|
||||
//zone = time.FixedZone("GMT", 8*3600)
|
||||
)
|
||||
|
||||
func (st *STimer) descChinese() string {
|
||||
format := "2006-01-02 15:04:05"
|
||||
var prefix string
|
||||
switch st.Type {
|
||||
case api.TIMER_TYPE_ONCE:
|
||||
return fmt.Sprintf("单次 %s触发", st.StartTime.In(zone).Format(format))
|
||||
case api.TIMER_TYPE_DAY:
|
||||
prefix = "每天"
|
||||
case api.TIMER_TYPE_WEEK:
|
||||
wds := st.GetWeekDays()
|
||||
weekDays := make([]string, len(wds))
|
||||
for i := range wds {
|
||||
weekDays[i] = fmt.Sprintf("星期%s", wdsCN[wds[i]])
|
||||
}
|
||||
prefix = fmt.Sprintf("每周 【%s】", strings.Join(weekDays, "|"))
|
||||
case api.TIMER_TYPE_MONTH:
|
||||
mns := st.GetMonthDays()
|
||||
monthDays := make([]string, len(mns))
|
||||
for i := range mns {
|
||||
monthDays[i] = fmt.Sprintf("%d号", mns[i])
|
||||
}
|
||||
prefix = fmt.Sprintf("每月 【%s】", strings.Join(monthDays, "|"))
|
||||
}
|
||||
return fmt.Sprintf("%s %02d:%02d触发 有效时间为%s至%s", prefix, st.Hour, st.Minute, st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
|
||||
}
|
||||
|
||||
func (st *STimer) descEnglish() string {
|
||||
var detail string
|
||||
format := "2006-01-02 15:04:05"
|
||||
switch st.Type {
|
||||
case api.TIMER_TYPE_ONCE:
|
||||
return st.EndTime.In(zone).Format(format)
|
||||
case api.TIMER_TYPE_DAY:
|
||||
detail = fmt.Sprintf("%d:%d every day", st.Hour, st.Minute)
|
||||
case api.TIMER_TYPE_WEEK:
|
||||
detail = st.weekDaysDesc()
|
||||
case api.TIMER_TYPE_MONTH:
|
||||
detail = st.monthDaysDesc()
|
||||
}
|
||||
if st.EndTime.IsZero() {
|
||||
return detail
|
||||
}
|
||||
return fmt.Sprintf("%s, from %s to %s", detail, st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
|
||||
}
|
||||
|
||||
func (st *STimer) weekDaysDesc() string {
|
||||
if st.WeekDays == 0 {
|
||||
return ""
|
||||
}
|
||||
var desc strings.Builder
|
||||
wds := st.GetWeekDays()
|
||||
i := 0
|
||||
desc.WriteString(fmt.Sprintf("%d:%d every %s", st.Hour, st.Minute, wdsEN[wds[i]]))
|
||||
for i++; i < len(wds)-1; i++ {
|
||||
desc.WriteString(", ")
|
||||
desc.WriteString(wdsEN[wds[i]])
|
||||
}
|
||||
if i == len(wds)-1 {
|
||||
desc.WriteString(" and ")
|
||||
desc.WriteString(wdsEN[wds[i]])
|
||||
}
|
||||
return desc.String()
|
||||
}
|
||||
|
||||
func (st *STimer) monthDaysDesc() string {
|
||||
if st.MonthDays == 0 {
|
||||
return ""
|
||||
}
|
||||
var desc strings.Builder
|
||||
mds := st.GetMonthDays()
|
||||
i := 0
|
||||
desc.WriteString(fmt.Sprintf("%d:%d on the %d%s", st.Hour, st.Minute, mds[i], st.dateSuffix(mds[i])))
|
||||
for i++; i < len(mds)-1; i++ {
|
||||
desc.WriteString(", ")
|
||||
desc.WriteString(strconv.Itoa(mds[i]))
|
||||
desc.WriteString(st.dateSuffix(mds[i]))
|
||||
}
|
||||
if i == len(mds)-1 {
|
||||
desc.WriteString(" and ")
|
||||
desc.WriteString(strconv.Itoa(mds[i]))
|
||||
desc.WriteString(st.dateSuffix(mds[i]))
|
||||
}
|
||||
desc.WriteString(" of each month")
|
||||
return desc.String()
|
||||
}
|
||||
|
||||
func (st *STimer) dateSuffix(date int) string {
|
||||
var ret string
|
||||
switch date {
|
||||
case 1:
|
||||
ret = "st"
|
||||
case 2:
|
||||
ret = "nd"
|
||||
case 3:
|
||||
ret = "rd"
|
||||
default:
|
||||
ret = "th"
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func checkCycleTimerCreateInput(in api.CycleTimerCreateInput) (api.CycleTimerCreateInput, error) {
|
||||
now := time.Now()
|
||||
if in.Minute < 0 || in.Minute > 59 {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
api "yunion.io/x/onecloud/pkg/apis/compute"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
|
||||
@@ -33,6 +34,7 @@ type GuestStopTask struct {
|
||||
|
||||
func init() {
|
||||
taskman.RegisterTask(GuestStopTask{})
|
||||
taskman.RegisterTask(GuestStopAndFreezeTask{})
|
||||
}
|
||||
|
||||
func (self *GuestStopTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
@@ -80,3 +82,40 @@ func (self *GuestStopTask) OnGuestStopTaskCompleteFailed(ctx context.Context, gu
|
||||
self.SetStageFailed(ctx, reason)
|
||||
logclient.AddActionLogWithStartable(self, guest, logclient.ACT_VM_STOP, reason.String(), self.UserCred, false)
|
||||
}
|
||||
|
||||
type GuestStopAndFreezeTask struct {
|
||||
SGuestBaseTask
|
||||
}
|
||||
|
||||
func (self *GuestStopAndFreezeTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) {
|
||||
guest := obj.(*models.SGuest)
|
||||
self.SetStage("OnStopGuest", nil)
|
||||
err := guest.StartGuestStopTask(ctx, self.UserCred, false, false, self.GetTaskId())
|
||||
if err != nil {
|
||||
self.OnStopGuestFailed(ctx, guest, jsonutils.NewString(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GuestStopAndFreezeTask) OnStopGuestFailed(ctx context.Context, guest *models.SGuest, reason jsonutils.JSONObject) {
|
||||
db.OpsLog.LogEvent(guest, db.ACT_FREEZE_FAIL, reason.String(), self.UserCred)
|
||||
self.SetStageFailed(ctx, reason)
|
||||
}
|
||||
|
||||
func (self *GuestStopAndFreezeTask) OnStopGuest(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) {
|
||||
self.SetStage("OnSyncStatus", nil)
|
||||
guest.StartSyncstatus(ctx, self.UserCred, self.GetTaskId())
|
||||
}
|
||||
|
||||
func (self *GuestStopAndFreezeTask) OnSyncStatus(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) {
|
||||
_, err := guest.SVirtualResourceBase.PerformFreeze(ctx, self.UserCred, nil, apis.PerformFreezeInput{})
|
||||
if err != nil {
|
||||
self.OnStopGuestFailed(ctx, guest, jsonutils.NewString(err.Error()))
|
||||
return
|
||||
}
|
||||
self.SetStageComplete(ctx, nil)
|
||||
}
|
||||
|
||||
func (self *GuestStopAndFreezeTask) OnSyncStatusFailed(ctx context.Context, guest *models.SGuest, reason jsonutils.JSONObject) {
|
||||
db.OpsLog.LogEvent(guest, db.ACT_FREEZE_FAIL, reason.String(), self.UserCred)
|
||||
self.SetStageFailed(ctx, reason)
|
||||
}
|
||||
|
||||
@@ -68,8 +68,13 @@ func (self *GuestSyncstatusTask) OnGetStatusSucc(ctx context.Context, guest *mod
|
||||
default:
|
||||
statusStr = api.VM_UNKNOWN
|
||||
}
|
||||
blockJobsCount, err := body.Int("block_jobs_count")
|
||||
if err != nil {
|
||||
blockJobsCount = -1
|
||||
}
|
||||
input := apis.PerformStatusInput{
|
||||
Status: statusStr,
|
||||
Status: statusStr,
|
||||
BlockJobsCount: int(blockJobsCount),
|
||||
}
|
||||
guest.PerformStatus(ctx, self.UserCred, nil, input)
|
||||
self.SetStageComplete(ctx, nil)
|
||||
|
||||
@@ -106,8 +106,12 @@ func guestActions(f actionFunc) appsrv.FilterHandler {
|
||||
|
||||
func getStatus(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
params, _, _ := appsrv.FetchEnv(ctx, w, r)
|
||||
var status = guestman.GetGuestManager().Status(params["<sid>"])
|
||||
appsrv.SendStruct(w, strDict{"status": status})
|
||||
var status, blockJobsCount = guestman.GetGuestManager().StatusWithBlockJobsCount(params["<sid>"])
|
||||
res := map[string]interface{}{
|
||||
"status": status,
|
||||
"block_jobs_count": blockJobsCount,
|
||||
}
|
||||
appsrv.SendStruct(w, res)
|
||||
}
|
||||
|
||||
func cpusetBalance(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -477,6 +477,19 @@ func (m *SGuestManager) Status(sid string) string {
|
||||
return status
|
||||
}
|
||||
|
||||
func (m *SGuestManager) StatusWithBlockJobsCount(sid string) (string, int) {
|
||||
status := m.GetStatus(sid)
|
||||
blockJobsCount := 0
|
||||
if status == GUEST_RUNNING {
|
||||
guest, _ := m.GetServer(sid)
|
||||
if guest.Monitor == nil && !guest.IsStopping() {
|
||||
guest.StartMonitor(context.Background())
|
||||
}
|
||||
blockJobsCount = guest.BlockJobsCount()
|
||||
}
|
||||
return status, blockJobsCount
|
||||
}
|
||||
|
||||
func (m *SGuestManager) GetStatus(sid string) string {
|
||||
if guest, ok := m.GetServer(sid); ok {
|
||||
if guest.IsRunning() && guest.Monitor != nil && guest.IsMaster() {
|
||||
|
||||
23
pkg/mcclient/modules/mod_analysis_predict.go
Normal file
23
pkg/mcclient/modules/mod_analysis_predict.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package modules
|
||||
|
||||
import "yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||||
|
||||
var (
|
||||
AnalysisPredictManager *SAnalysisPredict
|
||||
)
|
||||
|
||||
func init() {
|
||||
AnalysisPredictManager = NewAnalysisPredict()
|
||||
Register(AnalysisPredictManager)
|
||||
}
|
||||
|
||||
type SAnalysisPredict struct {
|
||||
*modulebase.ResourceManager
|
||||
}
|
||||
|
||||
func NewAnalysisPredict() *SAnalysisPredict {
|
||||
man := NewSuggestionManager("analysispredict", "analysispredicts", []string{}, []string{})
|
||||
return &SAnalysisPredict{
|
||||
ResourceManager: &man,
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ type AlertConfig struct {
|
||||
conditions []*AlertCondition
|
||||
execErrorState string
|
||||
noDataState string
|
||||
UsedBy string
|
||||
}
|
||||
|
||||
func NewAlertConfig(name string, frequency string, enabled bool) (*AlertConfig, error) {
|
||||
|
||||
@@ -29,3 +29,16 @@ func (o *AlertRecordShowOptions) Params() (jsonutils.JSONObject, error) {
|
||||
func (o *AlertRecordShowOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
type AlertRecordTotalOptions struct {
|
||||
ID string `help:"total-alert" json:"-"`
|
||||
options.BaseListOptions
|
||||
}
|
||||
|
||||
func (o *AlertRecordTotalOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.ListStructToParams(o)
|
||||
}
|
||||
|
||||
func (o *AlertRecordTotalOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
@@ -25,10 +25,22 @@ type SuggestSysAlertListOptions struct {
|
||||
Type string `help:"Type of suggest rule" choices:"EIP_UNUSED|"`
|
||||
}
|
||||
|
||||
func (o *SuggestSysAlertListOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.ListStructToParams(o)
|
||||
}
|
||||
|
||||
type SSuggestAlertShowOptions struct {
|
||||
ID string `help:"ID or name of the alert" json:"-"`
|
||||
}
|
||||
|
||||
func (o *SSuggestAlertShowOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.StructToParams(o)
|
||||
}
|
||||
|
||||
func (o *SSuggestAlertShowOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
type SuggestAlertIgnoreOptions struct {
|
||||
ID string `help:"ID or name of the alert" json:"-"`
|
||||
Scope string `help:"Resource scope" choices:"system|domain|project" default:"project"`
|
||||
@@ -36,6 +48,10 @@ type SuggestAlertIgnoreOptions struct {
|
||||
Project string `help:"'Owner project ID or Name" json:"project"`
|
||||
}
|
||||
|
||||
func (opt *SuggestAlertIgnoreOptions) Params() (*jsonutils.JSONDict, error) {
|
||||
func (opt *SuggestAlertIgnoreOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.StructToParams(opt)
|
||||
}
|
||||
|
||||
func (o *SuggestAlertIgnoreOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
24
pkg/mcclient/options/suggestion/analysispredict.go
Normal file
24
pkg/mcclient/options/suggestion/analysispredict.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package suggestion
|
||||
|
||||
import (
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient/options"
|
||||
)
|
||||
|
||||
type AnalysisPredictConfigOptions struct {
|
||||
ID string `help:"ID or name of the alert" json:"-"`
|
||||
options.BaseListOptions
|
||||
QueryType string `help:"query_type of the analysis" choices:"expense_trend" json:"query_type"`
|
||||
StartDate string `help:"start_date of the analysis" json:"start_date"`
|
||||
EndDate string `help:"end_date of the analysis" json:"end_date"`
|
||||
DataType string `help:"data_type of the analysis" choices:"day|month" json:"data_type"`
|
||||
}
|
||||
|
||||
func (o *AnalysisPredictConfigOptions) Params() (jsonutils.JSONObject, error) {
|
||||
return options.ListStructToParams(o)
|
||||
}
|
||||
|
||||
func (o *AnalysisPredictConfigOptions) GetId() string {
|
||||
return o.ID
|
||||
}
|
||||
1
pkg/mcclient/options/suggestion/doc.go
Normal file
1
pkg/mcclient/options/suggestion/doc.go
Normal file
@@ -0,0 +1 @@
|
||||
package suggestion // import "yunion.io/x/onecloud/pkg/mcclient/options/suggestion"
|
||||
@@ -193,14 +193,13 @@ func init() {
|
||||
// rds_cpu
|
||||
RegistryMetricCreateInput("rds_cpu", "Rds CPU usage", monitor.METRIC_RES_TYPE_RDS,
|
||||
monitor.METRIC_DATABASE_TELE, 1, []monitor.MetricFieldCreateInput{
|
||||
newMetricFieldCreateInput("used_percent", "Connection usage", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
newMetricFieldCreateInput("usage_active", "CPU active state utilization rate", monitor.METRIC_UNIT_PERCENT, 2),
|
||||
})
|
||||
|
||||
// rds_mem
|
||||
RegistryMetricCreateInput("rds_mem", "Rds memory", monitor.METRIC_RES_TYPE_RDS,
|
||||
monitor.METRIC_DATABASE_TELE, 2, []monitor.MetricFieldCreateInput{
|
||||
newMetricFieldCreateInput("used_percent", "Connection usage", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
newMetricFieldCreateInput("used_percent", "Used memory rate", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
})
|
||||
|
||||
// rds_netio
|
||||
@@ -219,13 +218,13 @@ func init() {
|
||||
// dcs_cpu
|
||||
RegistryMetricCreateInput("dcs_cpu", "Redis CPU usage", monitor.METRIC_RES_TYPE_REDIS,
|
||||
monitor.METRIC_DATABASE_TELE, 1, []monitor.MetricFieldCreateInput{
|
||||
newMetricFieldCreateInput("used_percent", "CPU active state utilization rate", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
newMetricFieldCreateInput("usage_percent", "CPU active state utilization rate", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
})
|
||||
|
||||
// dcs_mem
|
||||
RegistryMetricCreateInput("dcs_mem", "Redis memory", monitor.METRIC_RES_TYPE_REDIS,
|
||||
monitor.METRIC_DATABASE_TELE, 2, []monitor.MetricFieldCreateInput{
|
||||
newMetricFieldCreateInput("used_percent", "memory usage", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
newMetricFieldCreateInput("used_percent", "Used memory rate", monitor.METRIC_UNIT_PERCENT, 1),
|
||||
})
|
||||
|
||||
// dcs_netio
|
||||
|
||||
@@ -297,5 +297,51 @@ func (manager *SAlertRecordManager) DeleteRecordsOfThirtyDaysAgo(ctx context.Con
|
||||
log.Errorf("delete expire record:%s err:%v", records[i].GetId(), err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (manager *SAlertRecordManager) AllowGetPropertyTotalAlert(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (manager *SAlertRecordManager) GetPropertyTotalAlert(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
|
||||
alertRecords, err := manager.getNowAlertingRecord(ctx, userCred, query)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getNowAlertingRecord error")
|
||||
}
|
||||
alertCountMap := jsonutils.NewDict()
|
||||
for _, record := range alertRecords {
|
||||
evalMatches, err := record.GetEvalData()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "get record:%s evalData error", record.GetId())
|
||||
}
|
||||
count := int64(len(evalMatches))
|
||||
if alertCountMap.Contains(record.ResType) {
|
||||
resTypeCount, _ := alertCountMap.Int(record.ResType)
|
||||
count = count + resTypeCount
|
||||
}
|
||||
alertCountMap.Set(record.ResType, jsonutils.NewInt(count))
|
||||
}
|
||||
return alertCountMap, nil
|
||||
}
|
||||
|
||||
func (manager *SAlertRecordManager) getNowAlertingRecord(ctx context.Context, userCred mcclient.TokenCredential,
|
||||
param jsonutils.JSONObject) ([]SAlertRecord, error) {
|
||||
now := time.Now()
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 1, now.Location())
|
||||
query := manager.Query()
|
||||
scope, _ := param.GetString("scope")
|
||||
query = manager.FilterByOwner(query, userCred, rbacutils.String2Scope(scope))
|
||||
query = query.GE("created_at", startTime.UTC().Format(timeutils.MysqlTimeFormat))
|
||||
query = query.Equals("state", monitor.AlertStateAlerting)
|
||||
sQuery := CommonAlertManager.Query("id").Equals("state", monitor.AlertStateAlerting).IsNull("used_by").SubQuery()
|
||||
query = query.In("alert_id", sQuery).IsNotNull("res_type").IsNotEmpty("res_type").GroupBy("alert_id")
|
||||
records := make([]SAlertRecord, 0)
|
||||
err := db.FetchModelObjects(manager, query, &records)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ func init() {
|
||||
renders["last"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["max"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["min"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["abs"] = QueryDefinition{Renderer: functionRenderer}
|
||||
renders["percentile"] = QueryDefinition{
|
||||
Renderer: functionRenderer,
|
||||
Params: []DefinitionParameters{{Name: "nth", Type: "int"}},
|
||||
|
||||
@@ -1025,6 +1025,7 @@ func (host *SHost) CloneVM(ctx context.Context, from *SVirtualMachine, snapshot
|
||||
Device: dev,
|
||||
})
|
||||
}
|
||||
nicIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -533,6 +533,8 @@ func (rm *SReceiverManager) FetchOwnerId(ctx context.Context, data jsonutils.JSO
|
||||
return nil, errors.Wrap(err, "UserCacheManager.FetchUserByIdOrName")
|
||||
}
|
||||
ownerId := db.SOwnerId{
|
||||
DomainId: u.DomainId,
|
||||
Domain: u.Domain,
|
||||
UserDomain: u.Domain,
|
||||
UserDomainId: u.DomainId,
|
||||
UserId: u.Id,
|
||||
|
||||
@@ -178,6 +178,7 @@ func FileGetContents(file string) (string, error) {
|
||||
func GetFsFormat(diskPath string) string {
|
||||
ret, err := procutils.NewCommand("blkid", "-o", "value", "-s", "TYPE", diskPath).Output()
|
||||
if err != nil {
|
||||
log.Errorf("failed exec blkid of dev %s: %s, %s", diskPath, err, ret)
|
||||
return ""
|
||||
}
|
||||
var res string
|
||||
|
||||
@@ -44,7 +44,7 @@ func (t *SSplitTableSpec) Insert(dt interface{}) error {
|
||||
newMeta := false
|
||||
if len(metas) > 0 {
|
||||
lastMeta := metas[len(metas)-1]
|
||||
if lastDate.Sub(lastMeta.StartDate) > t.maxDuration {
|
||||
if !lastMeta.StartDate.IsZero() && lastDate.Sub(lastMeta.StartDate) > t.maxDuration {
|
||||
lastTable := t.GetTableSpec(lastMeta)
|
||||
ti := lastTable.Instance()
|
||||
q := ti.Query(sqlchemy.MAX("last_index", ti.Field(t.indexField)), sqlchemy.MAX("last_date", ti.Field(t.dateField)))
|
||||
@@ -64,29 +64,49 @@ func (t *SSplitTableSpec) Insert(dt interface{}) error {
|
||||
}
|
||||
newMeta = true
|
||||
} else {
|
||||
if lastMeta.StartDate.IsZero() {
|
||||
indexCol := t.tableSpec.ColumnSpec(t.indexField)
|
||||
_, err = t.metaSpec.Update(&lastMeta, func() error {
|
||||
lastMeta.Start = indexCol.(*sqlchemy.SIntegerColumn).AutoIncrementOffset
|
||||
lastMeta.StartDate = lastDate
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Update last meta")
|
||||
}
|
||||
}
|
||||
lastTableSpec = t.GetTableSpec(lastMeta)
|
||||
}
|
||||
} else {
|
||||
newMeta = true
|
||||
}
|
||||
if newMeta {
|
||||
// insert a new metadata
|
||||
meta := STableMetadata{
|
||||
Table: fmt.Sprintf("%s_%d", t.tableName, lastDate.Unix()),
|
||||
Start: lastRecIndex + 1,
|
||||
StartDate: lastDate,
|
||||
}
|
||||
err := t.metaSpec.Insert(&meta)
|
||||
lastTableSpec, err = t.newTable(lastRecIndex, lastDate)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "insert new meta")
|
||||
return errors.Wrap(err, "newTable")
|
||||
}
|
||||
// create new table
|
||||
newTable := t.GetTableSpec(meta)
|
||||
err = newTable.Sync()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "sync new table")
|
||||
}
|
||||
lastTableSpec = newTable
|
||||
}
|
||||
return lastTableSpec.Insert(dt)
|
||||
}
|
||||
|
||||
func (t *SSplitTableSpec) newTable(lastRecIndex int64, lastDate time.Time) (*sqlchemy.STableSpec, error) {
|
||||
// insert a new metadata
|
||||
meta := STableMetadata{
|
||||
Table: fmt.Sprintf("%s_%d", t.tableName, lastDate.Unix()),
|
||||
}
|
||||
if lastRecIndex > 0 {
|
||||
meta.Start = lastRecIndex + 1
|
||||
meta.StartDate = lastDate
|
||||
}
|
||||
err := t.metaSpec.Insert(&meta)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "insert new meta")
|
||||
}
|
||||
// create new table
|
||||
newTable := t.GetTableSpec(meta)
|
||||
err = newTable.Sync()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sync new table")
|
||||
}
|
||||
return newTable, nil
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func NewSplitTableSpec(s interface{}, name string, indexField string, dateField
|
||||
|
||||
metaSpec := sqlchemy.NewTableSpecFromStruct(&STableMetadata{}, fmt.Sprintf("%s_metadata", name))
|
||||
|
||||
return &SSplitTableSpec{
|
||||
sts := &SSplitTableSpec{
|
||||
indexField: indexField,
|
||||
dateField: dateField,
|
||||
tableName: name,
|
||||
@@ -148,5 +148,7 @@ func NewSplitTableSpec(s interface{}, name string, indexField string, dateField
|
||||
metaSpec: metaSpec,
|
||||
maxDuration: maxDuration,
|
||||
maxSegments: maxSegments,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return sts, nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ func (spec *SSplitTableSpec) Sync() error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "insert init metadata")
|
||||
}
|
||||
} else {
|
||||
_, err := spec.newTable(-1, time.Time{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "spec.newTable")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := range metas {
|
||||
@@ -81,11 +86,15 @@ func (spec *SSplitTableSpec) CheckSync() error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "GetTableMetas")
|
||||
}
|
||||
for i := range metas {
|
||||
subSpec := spec.GetTableSpec(metas[i])
|
||||
err := subSpec.CheckSync()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "GetTableSpec")
|
||||
if len(metas) == 0 {
|
||||
return errors.Wrap(err, "empty metadata")
|
||||
} else {
|
||||
for i := range metas {
|
||||
subSpec := spec.GetTableSpec(metas[i])
|
||||
err := subSpec.CheckSync()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "GetTableSpec")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -93,6 +102,8 @@ func (spec *SSplitTableSpec) CheckSync() error {
|
||||
|
||||
func (spec *SSplitTableSpec) SyncSQL() []string {
|
||||
sqls := spec.metaSpec.SyncSQL()
|
||||
zeroMeta := false
|
||||
|
||||
if spec.metaSpec.Exists() {
|
||||
metas, err := spec.GetTableMetas()
|
||||
if err != nil {
|
||||
@@ -105,7 +116,30 @@ func (spec *SSplitTableSpec) SyncSQL() []string {
|
||||
sqls = append(sqls, nsql...)
|
||||
}
|
||||
return sqls
|
||||
} else { // len(metas) == 0
|
||||
zeroMeta = true
|
||||
}
|
||||
} else {
|
||||
nsql := spec.metaSpec.SyncSQL()
|
||||
sqls = append(sqls, nsql...)
|
||||
zeroMeta = true
|
||||
}
|
||||
|
||||
if zeroMeta {
|
||||
indexCol := spec.tableSpec.ColumnSpec(spec.indexField)
|
||||
now := time.Now()
|
||||
meta := STableMetadata{
|
||||
Table: fmt.Sprintf("%s_%d", spec.tableName, now.Unix()),
|
||||
Start: indexCol.(*sqlchemy.SIntegerColumn).AutoIncrementOffset,
|
||||
}
|
||||
// insert the first meta
|
||||
sql := fmt.Sprintf("INSERT INTO `%s`(`table`, `deleted`, `created_at`) VALUES('%s', 0, '%s')", spec.metaSpec.Name(), meta.Table, timeutils.MysqlTime(now))
|
||||
sqls = append(sqls, sql)
|
||||
// create the first table
|
||||
newtable := spec.GetTableSpec(meta)
|
||||
nsql := newtable.SyncSQL()
|
||||
sqls = append(sqls, nsql...)
|
||||
return sqls
|
||||
}
|
||||
|
||||
fakeMeta := STableMetadata{
|
||||
|
||||
Reference in New Issue
Block a user