Files
cloudpods/pkg/cloudcommon/db/tenantcache.go
2020-04-11 06:50:48 +08:00

420 lines
13 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package db
import (
"context"
"database/sql"
"fmt"
"runtime/debug"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
identityapi "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
var (
DefaultProjectFetcher func(ctx context.Context, id string) (*STenant, error)
DefaultDomainFetcher func(ctx context.Context, id string) (*STenant, error)
DefaultProjectsFetcher func(ctx context.Context, idList []string, isDomain bool) map[string]STenant
)
type STenantCacheManager struct {
SKeystoneCacheObjectManager
}
type STenant struct {
SKeystoneCacheObject
}
func NewTenant(idStr string, name string, domainId string, domainName string) STenant {
return STenant{SKeystoneCacheObject: NewKeystoneCacheObject(idStr, name, domainId, domainName)}
}
func NewDomain(idStr, name string) STenant {
return NewTenant(idStr, name, identityapi.KeystoneDomainRoot, identityapi.KeystoneDomainRoot)
}
func (tenant *STenant) GetModelManager() IModelManager {
return TenantCacheManager
}
var TenantCacheManager *STenantCacheManager
func init() {
TenantCacheManager = &STenantCacheManager{NewKeystoneCacheObjectManager(STenant{}, "tenant_cache_tbl", "tenant", "tenants")}
// log.Debugf("Initialize tenant cache manager %s %s", TenantCacheManager.KeywordPlural(), TenantCacheManager)
TenantCacheManager.SetVirtualObject(TenantCacheManager)
DefaultProjectFetcher = TenantCacheManager.FetchTenantByIdOrName
DefaultDomainFetcher = TenantCacheManager.FetchDomainByIdOrName
DefaultProjectsFetcher = fetchProjects
}
func RegistUserCredCacheUpdater() {
auth.RegisterAuthHook(onAuthCompleteUpdateCache)
}
func onAuthCompleteUpdateCache(userCred mcclient.TokenCredential) {
TenantCacheManager.updateTenantCache(userCred)
UserCacheManager.updateUserCache(userCred)
}
func (manager *STenantCacheManager) InitializeData() error {
q := manager.Query().IsNullOrEmpty("domain_id")
tenants := make([]STenant, 0)
err := FetchModelObjects(manager, q, &tenants)
if err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "query")
}
for i := range tenants {
_, err := Update(&tenants[i], func() error {
tenants[i].DomainId = identityapi.DEFAULT_DOMAIN_ID
tenants[i].Domain = identityapi.DEFAULT_DOMAIN_NAME
return nil
})
if err != nil {
return errors.Wrap(err, "update")
}
}
return nil
}
func (manager *STenantCacheManager) updateTenantCache(userCred mcclient.TokenCredential) {
manager.Save(context.Background(), userCred.GetProjectId(), userCred.GetProjectName(),
userCred.GetProjectDomainId(), userCred.GetProjectDomain())
}
func (manager *STenantCacheManager) GetTenantQuery(fields ...string) *sqlchemy.SQuery {
return manager.Query(fields...).NotEquals("domain_id", identityapi.KeystoneDomainRoot)
}
func (manager *STenantCacheManager) GetDomainQuery(fields ...string) *sqlchemy.SQuery {
return manager.Query(fields...).Equals("domain_id", identityapi.KeystoneDomainRoot)
}
func (manager *STenantCacheManager) fetchTenant(ctx context.Context, idStr string, isDomain bool, noExpireCheck bool, filter func(q *sqlchemy.SQuery) *sqlchemy.SQuery) (*STenant, error) {
var q *sqlchemy.SQuery
if isDomain {
q = manager.GetDomainQuery()
} else {
q = manager.GetTenantQuery()
}
q = filter(q)
tcnt, err := q.CountWithError()
if err != nil {
return nil, errors.Wrap(err, "CountWithError")
}
if tcnt > 1 {
return nil, errors.Wrapf(httperrors.ErrDuplicateName, "duplicate tenant/domain name (%d)", tcnt)
}
tobj, err := NewModelObject(manager)
if err != nil {
return nil, errors.Wrap(err, "NewModelObject")
}
err = q.First(tobj)
if err != nil && err != sql.ErrNoRows {
return nil, errors.Wrap(err, "query")
} else if tobj != nil {
tenant := tobj.(*STenant)
if noExpireCheck || !tenant.IsExpired() {
return tenant, nil
}
}
if isDomain {
return manager.fetchDomainFromKeystone(ctx, idStr)
} else {
return manager.fetchTenantFromKeystone(ctx, idStr)
}
}
func (t *STenant) IsExpired() bool {
if t.LastCheck.IsZero() {
return true
}
now := time.Now().UTC()
if t.LastCheck.Add(consts.GetTenantCacheExpireSeconds()).Before(now) {
return true
}
return false
}
func (manager *STenantCacheManager) FetchTenantByIdOrName(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchTenant(ctx, idStr, false, false, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
if stringutils2.IsUtf8(idStr) {
return q.Equals("name", idStr)
} else {
return q.Filter(sqlchemy.OR(
sqlchemy.Equals(q.Field("id"), idStr),
sqlchemy.Equals(q.Field("name"), idStr),
))
}
})
}
func (manager *STenantCacheManager) FetchTenantById(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchTenantById(ctx, idStr, false)
}
func (manager *STenantCacheManager) FetchTenantByIdWithoutExpireCheck(ctx context.Context, idStr string) (*STenant, error) {
// noExpireCheck should be true
return manager.fetchTenantById(ctx, idStr, true)
}
func (manager *STenantCacheManager) fetchTenantById(ctx context.Context, idStr string, noExpireCheck bool) (*STenant, error) {
return manager.fetchTenant(ctx, idStr, false, noExpireCheck, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
return q.Filter(sqlchemy.Equals(q.Field("id"), idStr))
})
}
func (manager *STenantCacheManager) FetchTenantByName(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchTenant(ctx, idStr, false, false, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
return q.Filter(sqlchemy.Equals(q.Field("name"), idStr))
})
}
func (manager *STenantCacheManager) fetchTenantFromKeystone(ctx context.Context, idStr string) (*STenant, error) {
if len(idStr) == 0 {
log.Debugf("fetch empty tenant!!!!\n%s", debug.Stack())
return nil, fmt.Errorf("Empty idStr")
}
// It is to query all domain's project.
query := jsonutils.NewDict()
query.Set("scope", jsonutils.NewString("system"))
s := auth.GetAdminSession(ctx, consts.GetRegion(), "v1")
tenant, err := modules.Projects.GetById(s, idStr, query)
if err != nil {
if je, ok := err.(*httputils.JSONClientError); ok && je.Code == 404 {
return nil, sql.ErrNoRows
}
log.Errorf("fetch project %s fail %s", idStr, err)
return nil, errors.Wrap(err, "modules.Projects.Get")
}
tenantId, _ := tenant.GetString("id")
tenantName, _ := tenant.GetString("name")
domainId, _ := tenant.GetString("domain_id")
domainName, _ := tenant.GetString("project_domain")
// manager.Save(ctx, domainId, domainName, identityapi.KeystoneDomainRoot, identityapi.KeystoneDomainRoot)
return manager.Save(ctx, tenantId, tenantName, domainId, domainName)
}
func (manager *STenantCacheManager) FetchDomainByIdOrName(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchTenant(ctx, idStr, true, false, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
if stringutils2.IsUtf8(idStr) {
return q.Equals("name", idStr)
} else {
return q.Filter(sqlchemy.OR(
sqlchemy.Equals(q.Field("id"), idStr),
sqlchemy.Equals(q.Field("name"), idStr),
))
}
})
}
func (manager *STenantCacheManager) FetchDomainById(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchDomainById(ctx, idStr, false)
}
func (manager *STenantCacheManager) FetchDomainByIdWithoutExpireCheck(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchDomainById(ctx, idStr, true)
}
func (manager *STenantCacheManager) fetchDomainById(ctx context.Context, idStr string, noExpireCheck bool) (*STenant, error) {
return manager.fetchTenant(ctx, idStr, true, noExpireCheck, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
return q.Filter(sqlchemy.Equals(q.Field("id"), idStr))
})
}
func (manager *STenantCacheManager) FetchDomainByName(ctx context.Context, idStr string) (*STenant, error) {
return manager.fetchTenant(ctx, idStr, true, false, func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
return q.Filter(sqlchemy.Equals(q.Field("name"), idStr))
})
}
func (manager *STenantCacheManager) fetchDomainFromKeystone(ctx context.Context, idStr string) (*STenant, error) {
if len(idStr) == 0 {
log.Debugf("fetch empty domain!!!!\n%s", debug.Stack())
return nil, fmt.Errorf("Empty idStr")
}
s := auth.GetAdminSession(ctx, consts.GetRegion(), "v1")
tenant, err := modules.Domains.GetById(s, idStr, nil)
if err != nil {
if je, ok := err.(*httputils.JSONClientError); ok && je.Code == 404 {
return nil, sql.ErrNoRows
}
log.Errorf("fetch project %s fail %s", idStr, err)
return nil, errors.Wrap(err, "modules.Projects.Get")
}
tenantId, err := tenant.GetString("id")
tenantName, err := tenant.GetString("name")
return manager.Save(ctx, tenantId, tenantName, identityapi.KeystoneDomainRoot, identityapi.KeystoneDomainRoot)
}
func (manager *STenantCacheManager) Delete(ctx context.Context, idStr string) error {
lockman.LockRawObject(ctx, manager.KeywordPlural(), idStr)
defer lockman.ReleaseRawObject(ctx, manager.KeywordPlural(), idStr)
objo, err := manager.FetchById(idStr)
if err != nil && err != sql.ErrNoRows {
log.Errorf("FetchTenantbyId fail %s", err)
return err
}
if err == sql.ErrNoRows {
return nil
}
return objo.Delete(ctx, nil)
}
func (manager *STenantCacheManager) Save(ctx context.Context, idStr string, name string, domainId string, domain string) (*STenant, error) {
lockman.LockRawObject(ctx, manager.KeywordPlural(), idStr)
defer lockman.ReleaseRawObject(ctx, manager.KeywordPlural(), idStr)
objo, err := manager.FetchById(idStr)
if err != nil && err != sql.ErrNoRows {
log.Errorf("FetchTenantbyId fail %s", err)
return nil, err
}
now := time.Now().UTC()
if err == nil {
obj := objo.(*STenant)
if obj.Id == idStr && obj.Name == name && obj.Domain == domain && obj.DomainId == domainId {
Update(obj, func() error {
obj.LastCheck = now
return nil
})
return obj, nil
}
_, err = Update(obj, func() error {
obj.Id = idStr
obj.Name = name
obj.Domain = domain
obj.DomainId = domainId
obj.LastCheck = now
return nil
})
if err != nil {
return nil, err
} else {
return obj, nil
}
} else {
objm, err := NewModelObject(manager)
obj := objm.(*STenant)
obj.Id = idStr
obj.Name = name
obj.Domain = domain
obj.DomainId = domainId
obj.LastCheck = now
err = manager.TableSpec().InsertOrUpdate(obj)
if err != nil {
return nil, errors.Wrap(err, "InsertOrUpdate")
} else {
return obj, nil
}
}
}
/*func (manager *STenantCacheManager) GenerateProjectUserCred(ctx context.Context, projectName string) (mcclient.TokenCredential, error) {
project, err := manager.FetchTenantByIdOrName(ctx, projectName)
if err != nil {
return nil, err
}
return &mcclient.SSimpleToken{
Project: project.Name,
ProjectId: project.Id,
}, nil
}*/
func (tenant *STenant) GetDomain() string {
if len(tenant.Domain) == 0 {
return identityapi.DEFAULT_DOMAIN_NAME
}
return tenant.Domain
}
func (tenant *STenant) GetDomainId() string {
if len(tenant.DomainId) == 0 {
return identityapi.DEFAULT_DOMAIN_ID
}
return tenant.DomainId
}
func (manager *STenantCacheManager) findFirstProjectOfDomain(domainId string) (*STenant, error) {
q := manager.Query().Equals("domain_id", domainId)
tenant := STenant{}
tenant.SetModelManager(manager, &tenant)
err := q.First(&tenant)
if err != nil {
return nil, err
}
return &tenant, nil
}
func (manager *STenantCacheManager) fetchDomainTenantsFromKeystone(domainId string) error {
if len(domainId) == 0 {
log.Debugf("fetch empty domain!!!!")
debug.PrintStack()
return fmt.Errorf("Empty domainId")
}
s := auth.GetAdminSession(context.Background(), consts.GetRegion(), "v1")
params := jsonutils.Marshal(map[string]string{"domain_id": domainId})
tenants, err := modules.Projects.List(s, params)
if err != nil {
return errors.Wrap(err, "Projects.List")
}
for _, tenant := range tenants.Data {
tenantId, _ := tenant.GetString("id")
tenantName, _ := tenant.GetString("name")
domainId, _ := tenant.GetString("domain_id")
domainName, _ := tenant.GetString("project_domain")
_, err = manager.Save(context.Background(), tenantId, tenantName, domainId, domainName)
if err != nil {
return err
}
}
return nil
}
func (manager *STenantCacheManager) FindFirstProjectOfDomain(domainId string) (*STenant, error) {
tenant, err := manager.findFirstProjectOfDomain(domainId)
if err != nil {
if err == sql.ErrNoRows {
err = manager.fetchDomainTenantsFromKeystone(domainId)
if err != nil {
return nil, errors.Wrap(err, "fetchDomainTenantsFromKeystone")
}
return manager.findFirstProjectOfDomain(domainId)
}
return nil, errors.Wrap(err, "findFirstProjectOfDomain.queryFirst")
}
return tenant, nil
}