diff --git a/cmd/climc/shell/compute/keypairs.go b/cmd/climc/shell/compute/keypairs.go index daeb5a9e30..cb87c8ae44 100644 --- a/cmd/climc/shell/compute/keypairs.go +++ b/cmd/climc/shell/compute/keypairs.go @@ -15,167 +15,20 @@ package compute import ( - "fmt" - "io/ioutil" - - "yunion.io/x/jsonutils" - - "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/cmd/climc/shell" modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" "yunion.io/x/onecloud/pkg/mcclient/options" + "yunion.io/x/onecloud/pkg/mcclient/options/compute" ) func init() { - type KeypairList struct { - options.BaseListOptions - } - - R(&KeypairList{}, "keypair-list", "List keypairs.", func(s *mcclient.ClientSession, args *KeypairList) error { - var params *jsonutils.JSONDict - { - var err error - params, err = args.BaseListOptions.Params() - if err != nil { - return err - - } - } - - result, err := modules.Keypairs.List(s, params) - if err != nil { - return err - } - - printList(result, modules.Keypairs.GetColumns(s)) - return nil - }) - - type KeypairCreate struct { - NAME string `help:"Name of keypair to be created"` - Scheme string `help:"Scheme of keypair, default is RSA" choices:"RSA" default:"RSA"` - PublicKey string `help:"Publickey of keypair"` - Desc string `help:"Short description of keypair"` - } - - R(&KeypairCreate{}, "keypair-create", "Create a new keypair", func(s *mcclient.ClientSession, args *KeypairCreate) error { - params := jsonutils.NewDict() - params.Add(jsonutils.NewString(args.NAME), "name") - if len(args.Scheme) > 0 { - params.Add(jsonutils.NewString(args.Scheme), "scheme") - } - - if len(args.PublicKey) > 0 { - params.Add(jsonutils.NewString(args.PublicKey), "public_key") - } - - if len(args.Desc) > 0 { - params.Add(jsonutils.NewString(args.Desc), "description") - } - - result, e := modules.Keypairs.Create(s, params) - if e != nil { - return e - } - printObject(result) - return nil - }) - - type KeypairUpdate struct { - ID string `help:"ID of keypair to be updated"` - Name string `help:"New name of keypair"` - Desc string `help:"Short description of keypair"` - } - - R(&KeypairUpdate{}, "keypair-update", "Update a keypair", func(s *mcclient.ClientSession, args *KeypairUpdate) error { - params := jsonutils.NewDict() - if len(args.Name) > 0 { - params.Add(jsonutils.NewString(args.Name), "name") - } - - if len(args.Desc) > 0 { - params.Add(jsonutils.NewString(args.Desc), "description") - } - - result, e := modules.Keypairs.Update(s, args.ID, params) - if e != nil { - return e - } - printObject(result) - return nil - }) - - type KeypairDelete struct { - ID string `help:"ID of keypair to be deleted"` - } - - R(&KeypairDelete{}, "keypair-delete", "Delete a keypair", func(s *mcclient.ClientSession, args *KeypairDelete) error { - result, e := modules.Keypairs.Delete(s, args.ID, nil) - if e != nil { - return e - } - printObject(result) - return nil - }) - - type KeypairShow struct { - ID string `help:"ID of keypair to be shown"` - } - - R(&KeypairShow{}, "keypair-show", "Show details of a keypair", func(s *mcclient.ClientSession, args *KeypairShow) error { - result, e := modules.Keypairs.Get(s, args.ID, nil) - if e != nil { - return e - } - printObject(result) - return nil - }) - - type KeypairImport struct { - NAME string `help:"Name of keypair to be imported"` - PublicKey string `help:"Filename of public key file, or public key can be supplied via stdin"` - Desc string `help:"Short description of keypair"` - } - - R(&KeypairImport{}, "keypair-import", "Create a new keypair with a existing public key", func(s *mcclient.ClientSession, args *KeypairImport) error { - params := jsonutils.NewDict() - params.Add(jsonutils.NewString(args.NAME), "name") - if len(args.PublicKey) > 0 { - content, e := ioutil.ReadFile(args.PublicKey) - if e != nil { - params.Add(jsonutils.NewString(args.PublicKey), "public_key") - } else { - params.Add(jsonutils.NewString(string(content)), "public_key") - } - } else { - return fmt.Errorf("no public key provided") - } - - if len(args.Desc) > 0 { - params.Add(jsonutils.NewString(args.Desc), "description") - } - - result, e := modules.Keypairs.Create(s, params) - if e != nil { - return e - } - printObject(result) - return nil - }) - - type KeypairPrivateKey struct { - ID string `help:"ID of keypair to fetch"` - } - - R(&KeypairPrivateKey{}, "keypair-privatekey", "Fetch the private key of a keypair, this can be done once only", func(s *mcclient.ClientSession, args *KeypairPrivateKey) error { - result, e := modules.Keypairs.GetSpecific(s, args.ID, "privatekey", nil) - if e != nil { - return e - } - key, e := result.GetString("private_key") - if e != nil { - return fmt.Errorf("Private key has been fetched") - } - fmt.Printf("%s", key) - return nil - }) + cmd := shell.NewResourceCmd(&modules.Keypairs) + cmd.List(&compute.KeypairList{}) + cmd.Create(&compute.KeypairCreate{}) + cmd.Update(&compute.KeypairUpdate{}) + cmd.Delete(&compute.KeyPairIdOptions{}) + cmd.Show(&compute.KeyPairIdOptions{}) + cmd.Get("privatekey", &compute.KeyPairIdOptions{}) + cmd.Perform("public", &options.BasePublicOptions{}) + cmd.Perform("private", &compute.KeyPairIdOptions{}) } diff --git a/pkg/apis/compute/input.go b/pkg/apis/compute/input.go index 2c91eb992b..fa63f8ef52 100644 --- a/pkg/apis/compute/input.go +++ b/pkg/apis/compute/input.go @@ -38,6 +38,7 @@ type DeletePreventableCreateInput struct { type KeypairListInput struct { apis.UserResourceListInput + apis.SharableResourceBaseListInput // 加密类型 // example: RSA diff --git a/pkg/apis/compute/keypair.go b/pkg/apis/compute/keypair.go index 9bfff95407..a0c5596bd5 100644 --- a/pkg/apis/compute/keypair.go +++ b/pkg/apis/compute/keypair.go @@ -40,6 +40,7 @@ type KeypairCreateInput struct { type KeypairDetails struct { apis.UserResourceDetails + apis.SharableResourceBaseInfo SKeypair // 私钥长度 diff --git a/pkg/cloudcommon/db/sharablebase.go b/pkg/cloudcommon/db/sharablebase.go index 147d52b890..01489b88ab 100644 --- a/pkg/cloudcommon/db/sharablebase.go +++ b/pkg/cloudcommon/db/sharablebase.go @@ -217,7 +217,47 @@ func SharableManagerValidateCreateData( func SharableManagerFilterByOwner(manager IStandaloneModelManager, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, owner mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery { if owner != nil { resScope := manager.ResourceScope() - if resScope == rbacscope.ScopeProject && scope == rbacscope.ScopeProject { + if resScope == rbacscope.ScopeUser { + targetProjectId := owner.GetProjectId() + if len(targetProjectId) == 0 { + targetProjectId = userCred.GetProjectId() + } + subq := SharedResourceManager.Query("resource_id") + subq = subq.Equals("resource_type", manager.Keyword()) + subq = subq.Equals("target_project_id", targetProjectId) + subq = subq.Equals("target_type", SharedTargetProject) + subq2 := SharedResourceManager.Query("resource_id") + subq2 = subq2.Equals("resource_type", manager.Keyword()) + subq2 = subq2.Equals("target_project_id", owner.GetProjectDomainId()) + subq2 = subq2.Equals("target_type", SharedTargetDomain) + filters := []sqlchemy.ICondition{ + sqlchemy.AND( + sqlchemy.IsTrue(q.Field("is_public")), + sqlchemy.Equals(q.Field("public_scope"), rbacscope.ScopeSystem), + ), + sqlchemy.AND( + sqlchemy.IsTrue(q.Field("is_public")), + sqlchemy.Equals(q.Field("public_scope"), rbacscope.ScopeDomain), + sqlchemy.OR( + sqlchemy.In(q.Field("id"), subq2.SubQuery()), + ), + ), + sqlchemy.In(q.Field("id"), subq.SubQuery()), + } + ownerUserId := owner.GetUserId() + if len(ownerUserId) > 0 { + filters = append(filters, sqlchemy.Equals(q.Field("owner_id"), ownerUserId)) + } + q = q.Filter(sqlchemy.OR(filters...)) + if userCred != nil { + result := policy.PolicyManager.Allow(scope, userCred, consts.GetServiceType(), manager.KeywordPlural(), policy.PolicyActionList) + if !result.ObjectTags.IsEmpty() { + policyTagFilters := tagutils.STagFilters{} + policyTagFilters.AddFilters(result.ObjectTags) + q = ObjectIdQueryWithTagFilters(q, "id", manager.Keyword(), policyTagFilters) + } + } + } else if resScope == rbacscope.ScopeProject && scope == rbacscope.ScopeProject { ownerProjectId := owner.GetProjectId() if len(ownerProjectId) > 0 { subq := SharedResourceManager.Query("resource_id") diff --git a/pkg/cloudcommon/db/sharedresource.go b/pkg/cloudcommon/db/sharedresource.go index 1fd37b3c45..f8dea9d74d 100644 --- a/pkg/cloudcommon/db/sharedresource.go +++ b/pkg/cloudcommon/db/sharedresource.go @@ -99,6 +99,14 @@ func (manager *SSharedResourceManager) shareToTarget( var requireScope rbacscope.TRbacScope resScope := model.GetModelManager().ResourceScope() switch resScope { + case rbacscope.ScopeUser: + switch targetType { + case SharedTargetDomain: + // should have system-level privileges + requireScope = rbacscope.ScopeSystem + case SharedTargetProject: + requireScope = rbacscope.ScopeDomain + } case rbacscope.ScopeProject: switch targetType { case SharedTargetProject: diff --git a/pkg/compute/models/keypairs.go b/pkg/compute/models/keypairs.go index 53a8adc86c..50e8b31947 100644 --- a/pkg/compute/models/keypairs.go +++ b/pkg/compute/models/keypairs.go @@ -22,10 +22,12 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/gotypes" "yunion.io/x/pkg/util/rbacscope" "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" + "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/httperrors" @@ -37,6 +39,7 @@ import ( type SKeypairManager struct { db.SUserResourceBaseManager + db.SSharableBaseResourceManager } var KeypairManager *SKeypairManager @@ -55,6 +58,7 @@ func init() { type SKeypair struct { db.SUserResourceBase + db.SSharableBaseResource // 加密类型 // example: RSA @@ -68,6 +72,14 @@ type SKeypair struct { PublicKey string `width:"1024" charset:"ascii" nullable:"false" list:"user" create:"required"` } +func (manager *SKeypairManager) GetISharableVirtualModelManager() db.ISharableVirtualModelManager { + return manager.GetVirtualObject().(db.ISharableVirtualModelManager) +} + +func (manager *SKeypairManager) GetIVirtualModelManager() db.IVirtualModelManager { + return manager.GetVirtualObject().(db.IVirtualModelManager) +} + // 列出ssh密钥对 func (manager *SKeypairManager) ListItemFilter( ctx context.Context, @@ -75,11 +87,27 @@ func (manager *SKeypairManager) ListItemFilter( userCred mcclient.TokenCredential, query api.KeypairListInput, ) (*sqlchemy.SQuery, error) { - q, err := manager.SUserResourceBaseManager.ListItemFilter(ctx, q, userCred, query.UserResourceListInput) + q, err := manager.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, query.StandaloneResourceListInput) if err != nil { return nil, err } + if ((query.Admin != nil && *query.Admin) || query.Scope == string(rbacscope.ScopeSystem)) && db.IsAdminAllowList(userCred, manager).Result.IsAllow() { + user := query.UserId + if len(user) > 0 { + uc, _ := db.UserCacheManager.FetchUserByIdOrName(ctx, user) + if uc == nil { + return nil, httperrors.NewUserNotFoundError("user %s not found", user) + } + q = q.Equals("owner_id", uc.Id) + } + } else { + q, err = manager.SSharableBaseResourceManager.ListItemFilter(ctx, q, userCred, query.SharableResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SSharableBaseResourceManager.ListItemFilter") + } + } + if len(query.Scheme) > 0 { q = q.In("scheme", query.Scheme) } @@ -87,6 +115,8 @@ func (manager *SKeypairManager) ListItemFilter( q = q.In("fingerprint", query.Fingerprint) } + q.DebugQuery() + return q, nil } @@ -113,6 +143,56 @@ func (manager *SKeypairManager) QueryDistinctExtraField(q *sqlchemy.SQuery, fiel return q, httperrors.ErrNotFound } +func (km *SKeypairManager) query(manager db.IModelManager, field string, keyIds []string, filter func(*sqlchemy.SQuery) *sqlchemy.SQuery) *sqlchemy.SSubQuery { + q := manager.Query() + + if filter != nil { + q = filter(q) + } + + sq := q.SubQuery() + + return sq.Query( + sq.Field("keypair_id"), + sqlchemy.COUNT(field), + ).In("keypair_id", keyIds).GroupBy(sq.Field("keypir_id")).SubQuery() +} + +type SKeypairUsageCount struct { + Id string + LinkedGuestCount int +} + +func (km *SKeypairManager) TotalResourceCount(keyIds []string) (map[string]SKeypairUsageCount, error) { + ret := map[string]SKeypairUsageCount{} + + guestSQ := km.query(GuestManager, "guest_cnt", keyIds, func(q *sqlchemy.SQuery) *sqlchemy.SQuery { + return q.IsNotEmpty("keypair_id") + }) + + keypairs := km.Query().SubQuery() + keypairsQ := keypairs.Query( + sqlchemy.SUM("linked_guest_count", guestSQ.Field("guest_cnt")), + ) + + keypairsQ.AppendField(keypairsQ.Field("id")) + + keypairsQ = keypairsQ.LeftJoin(guestSQ, sqlchemy.Equals(keypairsQ.Field("id"), guestSQ.Field("keypair_id"))) + + keypairsQ = keypairsQ.Filter(sqlchemy.In(keypairsQ.Field("id"), keyIds)).GroupBy(keypairsQ.Field("id")) + + counts := []SKeypairUsageCount{} + err := keypairsQ.All(&counts) + if err != nil { + return nil, errors.Wrapf(err, "keyparisQ.All") + } + for i := range counts { + ret[counts[i].Id] = counts[i] + } + + return ret, nil +} + func (manager *SKeypairManager) FetchCustomizeColumns( ctx context.Context, userCred mcclient.TokenCredential, @@ -123,13 +203,26 @@ func (manager *SKeypairManager) FetchCustomizeColumns( ) []api.KeypairDetails { rows := make([]api.KeypairDetails, len(objs)) userRows := manager.SUserResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + shareRows := manager.SSharableBaseResourceManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + keyIds := make([]string, len(objs)) for i := range rows { keypair := objs[i].(*SKeypair) rows[i] = api.KeypairDetails{ - UserResourceDetails: userRows[i], - PrivateKeyLen: len(keypair.PrivateKey), + UserResourceDetails: userRows[i], + SharableResourceBaseInfo: shareRows[i], + PrivateKeyLen: len(keypair.PrivateKey), + } + keyIds[i] = keypair.Id + } + + usages, err := manager.TotalResourceCount(keyIds) + if err != nil { + return rows + } + for i := range rows { + if cnt, ok := usages[keyIds[i]]; ok { + rows[i].LinkedGuestCount = cnt.LinkedGuestCount } - rows[i].LinkedGuestCount, _ = keypair.GetLinkedGuestsCount() } return rows @@ -178,12 +271,16 @@ func (manager *SKeypairManager) ValidateCreateData(ctx context.Context, userCred return input, nil } -func (self *SKeypair) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { - guestCnt, err := self.GetLinkedGuestsCount() - if err != nil { - return httperrors.NewInternalServerError("GetLinkedGuestsCount failed %s", err) +func (self *SKeypair) ValidateDeleteCondition(ctx context.Context, info *api.KeypairDetails) error { + if gotypes.IsNil(info) { + info = &api.KeypairDetails{} + var err error + info.LinkedGuestCount, err = self.GetLinkedGuestsCount() + if err != nil { + return httperrors.NewInternalServerError("GetLinkedGuestsCount failed %s", err) + } } - if guestCnt > 0 { + if info.LinkedGuestCount > 0 { return httperrors.NewNotEmptyError("Cannot delete keypair used by servers") } return self.SStandaloneResourceBase.ValidateDeleteCondition(ctx, nil) @@ -195,14 +292,7 @@ func totalKeypairCount(userId string) (int, error) { } func (manager *SKeypairManager) FilterByOwner(q *sqlchemy.SQuery, man db.FilterByOwnerProvider, userCred mcclient.TokenCredential, owner mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery { - if owner != nil { - if scope == rbacscope.ScopeUser { - if len(owner.GetUserId()) > 0 { - q = q.Equals("owner_id", owner.GetUserId()) - } - } - } - return q + return db.SharableManagerFilterByOwner(manager.GetISharableVirtualModelManager(), q, userCred, owner, scope) } func (keypair *SKeypair) GetDetailsPrivatekey(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { @@ -224,3 +314,82 @@ func (keypair *SKeypair) GetDetailsPrivatekey(ctx context.Context, userCred mccl } return retval, nil } + +func (self *SKeypair) GetOwnerId() mcclient.IIdentityProvider { + owner := &db.SOwnerId{UserId: self.OwnerId} + obj, err := db.UserCacheManager.FetchById(self.OwnerId) + if err != nil { + return owner + } + user := obj.(*db.SUser) + owner.DomainId = user.DomainId + return owner +} + +func (self *SKeypair) GetProjectDomainId() string { + obj, err := db.UserCacheManager.FetchById(self.OwnerId) + if err != nil { + return "" + } + user := obj.(*db.SUser) + return user.DomainId +} + +func (self *SKeypair) GetRequiredSharedDomainIds() []string { + obj, err := db.UserCacheManager.FetchById(self.OwnerId) + if err != nil { + return []string{} + } + user := obj.(*db.SUser) + return []string{user.DomainId} +} + +func (self *SKeypair) GetSharableTargetDomainIds() []string { + return []string{} +} + +func (self *SKeypair) GetChangeOwnerRequiredDomainIds() []string { + domainId := self.GetProjectDomainId() + if len(domainId) > 0 { + return []string{domainId} + } + return []string{} +} + +func (self *SKeypair) GetChangeOwnerCandidateDomainIds() []string { + domains := []db.STenant{} + db.TenantCacheManager.GetDomainQuery().All(&domains) + ret := []string{} + for i := range domains { + ret = append(ret, domains[i].Id) + } + return ret +} + +func (self *SKeypair) GetSharedDomains() []string { + return db.SharableGetSharedProjects(self, db.SharedTargetDomain) +} + +func (keypair *SKeypair) GetISharableModel() db.ISharableBaseModel { + return keypair.GetVirtualObject().(db.ISharableBaseModel) +} + +func (keypair *SKeypair) GetDetailsChangeOwnerCandidateDomains(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (apis.ChangeOwnerCandidateDomainsOutput, error) { + return db.IOwnerResourceBaseModelGetChangeOwnerCandidateDomains(keypair) +} + +func (self *SKeypair) PerformPublic(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformPublicProjectInput) (jsonutils.JSONObject, error) { + err := db.SharablePerformPublic(self.GetISharableModel(), ctx, userCred, input) + if err != nil { + return nil, errors.Wrap(err, "SharablePerformPublic") + } + return nil, nil +} + +func (self *SKeypair) PerformPrivate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.PerformPrivateInput) (jsonutils.JSONObject, error) { + err := db.SharablePerformPrivate(self.GetISharableModel(), ctx, userCred) + if err != nil { + return nil, errors.Wrap(err, "SharablePerformPrivate") + } + return nil, nil +} diff --git a/pkg/mcclient/options/base.go b/pkg/mcclient/options/base.go index 94bc5fed98..6a6e4e8714 100644 --- a/pkg/mcclient/options/base.go +++ b/pkg/mcclient/options/base.go @@ -468,9 +468,10 @@ func (opts *BaseUpdateOptions) Params() (jsonutils.JSONObject, error) { } type BasePublicOptions struct { - ID string `help:"ID or name of resource" json:"-"` - Scope string `help:"sharing scope" choices:"system|domain"` - SharedDomains []string `help:"share to domains"` + ID string `help:"ID or name of resource" json:"-"` + Scope string `help:"sharing scope" choices:"system|domain|project"` + SharedDomains []string `help:"share to domains"` + SharedProjects []string `help:"share to projects"` } func (opts *BasePublicOptions) GetId() string { diff --git a/pkg/mcclient/options/compute/keypairs.go b/pkg/mcclient/options/compute/keypairs.go new file mode 100644 index 0000000000..d0c2ccbc12 --- /dev/null +++ b/pkg/mcclient/options/compute/keypairs.go @@ -0,0 +1,83 @@ +// 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 compute + +import ( + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +type KeypairList struct { + options.BaseListOptions +} + +func (self *KeypairList) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(self) +} + +type KeypairCreate struct { + NAME string `help:"Name of keypair to be created"` + Scheme string `help:"Scheme of keypair, default is RSA" choices:"RSA" default:"RSA"` + PublicKey string `help:"Publickey of keypair"` + Desc string `help:"Short description of keypair"` +} + +func (args *KeypairCreate) Params() (jsonutils.JSONObject, error) { + params := jsonutils.NewDict() + params.Add(jsonutils.NewString(args.NAME), "name") + if len(args.Scheme) > 0 { + params.Add(jsonutils.NewString(args.Scheme), "scheme") + } + + if len(args.PublicKey) > 0 { + params.Add(jsonutils.NewString(args.PublicKey), "public_key") + } + + if len(args.Desc) > 0 { + params.Add(jsonutils.NewString(args.Desc), "description") + } + return params, nil +} + +type KeyPairIdOptions struct { + ID string `help:"ID of keypair"` +} + +func (self *KeyPairIdOptions) GetId() string { + return self.ID +} + +func (self *KeyPairIdOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type KeypairUpdate struct { + KeyPairIdOptions + Name string `help:"New name of keypair"` + Desc string `help:"Short description of keypair"` +} + +func (args *KeypairUpdate) Params() (jsonutils.JSONObject, error) { + params := jsonutils.NewDict() + if len(args.Name) > 0 { + params.Add(jsonutils.NewString(args.Name), "name") + } + + if len(args.Desc) > 0 { + params.Add(jsonutils.NewString(args.Desc), "description") + } + return params, nil +}