diff --git a/cmd/climc/shell/compute/app.go b/cmd/climc/shell/compute/app.go new file mode 100644 index 0000000000..cff6e79488 --- /dev/null +++ b/cmd/climc/shell/compute/app.go @@ -0,0 +1,32 @@ +// 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/onecloud/cmd/climc/shell" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options/compute" +) + +func init() { + cmd := shell.NewResourceCmd(&modules.Apps) + cmd.List(&compute.AppListOptions{}) + cmd.Show(&compute.AppIdOptions{}) + cmd.Perform("syncstatus", &compute.AppIdOptions{}) + + cmd1 := shell.NewResourceCmd(&modules.AppEnvironments) + cmd1.List(&compute.AppEnvironmentListOptions{}) + cmd1.Show(&compute.AppEnvironmentIdOption{}) +} diff --git a/pkg/apis/compute/app.go b/pkg/apis/compute/app.go new file mode 100644 index 0000000000..5699445003 --- /dev/null +++ b/pkg/apis/compute/app.go @@ -0,0 +1,53 @@ +// 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/onecloud/pkg/apis" + +const ( + APP_TYPE_WEB = "web" + + APP_STATUS_READY = "ready" + APP_STATUS_UNKNOWN = "unknown" +) + +type AppListInput struct { + apis.VirtualResourceListInput + apis.ExternalizedResourceBaseListInput + apis.EnabledResourceBaseListInput + + ManagedResourceListInput + RegionalFilterListInput + + TechStack string `json:"tech_stack"` +} + +type AppDetails struct { + apis.VirtualResourceDetails + ManagedResourceInfo + CloudregionResourceInfo +} + +type AppEnvironmentListInput struct { + apis.VirtualResourceListInput + apis.ExternalizedResourceBaseListInput + + AppId string + InstanceType string +} + +type AppEnvironmentDetails struct { + apis.VirtualResourceDetails +} diff --git a/pkg/cloudprovider/consts.go b/pkg/cloudprovider/consts.go index 15dcf3177d..aab33e96be 100644 --- a/pkg/cloudprovider/consts.go +++ b/pkg/cloudprovider/consts.go @@ -60,6 +60,7 @@ const ( CLOUD_CAPABILITY_MONGO_DB = "mongodb" // MongoDB CLOUD_CAPABILITY_ES = "es" // ElasticSearch CLOUD_CAPABILITY_KAFKA = "kafka" // Kafka + CLOUD_CAPABILITY_APP = "app" //App ) const ( diff --git a/pkg/cloudprovider/resources.go b/pkg/cloudprovider/resources.go index 1936cd41ce..dc4976ab6e 100644 --- a/pkg/cloudprovider/resources.go +++ b/pkg/cloudprovider/resources.go @@ -183,6 +183,9 @@ type ICloudRegion interface { GetICloudKafkas() ([]ICloudKafka, error) GetICloudKafkaById(id string) (ICloudKafka, error) + + GetICloudApps() ([]ICloudApp, error) + GetICloudAppById(id string) (ICloudApp, error) } type ICloudZone interface { @@ -1443,3 +1446,18 @@ type ICloudKafka interface { Delete() error } + +type ICloudApp interface { + IVirtualResource + GetEnvironments() ([]ICloudAppEnvironment, error) + GetTechStack() string + GetType() string + GetKind() string + GetOsType() string +} + +type ICloudAppEnvironment interface { + IVirtualResource + GetInstanceType() (string, error) + GetInstanceNumber() (int, error) +} diff --git a/pkg/compute/models/app.go b/pkg/compute/models/app.go new file mode 100644 index 0000000000..22145fa79f --- /dev/null +++ b/pkg/compute/models/app.go @@ -0,0 +1,407 @@ +// 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 models + +import ( + "context" + "fmt" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/tristate" + "yunion.io/x/pkg/util/compare" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +type SAppManager struct { + db.SVirtualResourceBaseManager + db.SExternalizedResourceBaseManager + db.SEnabledResourceBaseManager + + SManagedResourceBaseManager + SCloudregionResourceBaseManager +} + +type SApp struct { + db.SVirtualResourceBase + db.SExternalizedResourceBase + db.SEnabledResourceBase + + SManagedResourceBase + SCloudregionResourceBase + + Type string `width:"16" charset:"ascii" nullable:"false" get:"user" list:"user"` + TechStack string `width:"64" charset:"ascii" nullable:"false" get:"user" list:"user"` + Kind string `width:"64" charset:"ascii" nullable:"false" get:"user" list:"user"` +} + +var AppManager *SAppManager + +func init() { + AppManager = &SAppManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SApp{}, + "apps_tbl", + "webapp", + "webapps", + ), + } + AppManager.SetVirtualObject(AppManager) +} + +func (am *SAppManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { + return true +} + +func (am *SAppManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.AppListInput) (*sqlchemy.SQuery, error) { + q, err := am.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter") + } + q, err = am.SEnabledResourceBaseManager.ListItemFilter(ctx, q, userCred, query.EnabledResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SEnabledResourceBaseManager.ListItemFilter") + } + q, err = am.SExternalizedResourceBaseManager.ListItemFilter(ctx, q, userCred, query.ExternalizedResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SExternalizedResourceBaseManager.ListItemFilter") + } + q, err = am.SManagedResourceBaseManager.ListItemFilter(ctx, q, userCred, query.ManagedResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.ListItemFilter") + } + q, err = am.SCloudregionResourceBaseManager.ListItemFilter(ctx, q, userCred, query.RegionalFilterListInput) + if err != nil { + return nil, errors.Wrap(err, "SCloudregionResourceBaseManager.ListItemFilter") + } + if query.TechStack != "" { + q = q.Equals("tech_stack", query.TechStack) + } + + return q, nil +} + +func (am *SAppManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.AppListInput) (*sqlchemy.SQuery, error) { + var err error + + q, err = am.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.OrderByExtraFields") + } + q, err = am.SManagedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.ManagedResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.OrderByExtraFields") + } + q, err = am.SCloudregionResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.RegionalFilterListInput) + if err != nil { + return nil, errors.Wrap(err, "CloudregionResourceBaseManager.OrderByExtraFields") + } + return q, nil +} + +func (am *SAppManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { + var err error + + q, err = am.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + q, err = am.SManagedResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + q, err = am.SCloudregionResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + switch field { + case "tech_stack": + q = am.Query("tech_stack").Distinct() + } + return q, nil +} + +func (am *SAppManager) ListItemExportKeys(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, keys stringutils2.SSortedStrings) (*sqlchemy.SQuery, error) { + var err error + q, err = am.SVirtualResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemExportKeys") + } + if keys.ContainsAny(am.SManagedResourceBaseManager.GetExportKeys()...) { + q, err = am.SManagedResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, errors.Wrap(err, "SManagedResourceBaseManager.ListItemExportKeys") + } + } + + if keys.ContainsAny(am.SCloudregionResourceBaseManager.GetExportKeys()...) { + q, err = am.SCloudregionResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, errors.Wrap(err, "SCloudregionResourceBaseManager.ListItemExportKeys") + } + } + + return q, nil +} + +func (am *SAppManager) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, isList bool) (api.AppDetails, error) { + return api.AppDetails{}, nil +} + +func (a *SApp) GetAppEnvironments() ([]SAppEnvironment, error) { + q := AppEnvironmentManager.Query().Equals("app_id", a.Id) + ret := []SAppEnvironment{} + err := db.FetchModelObjects(AppEnvironmentManager, q, &ret) + if err != nil { + return nil, errors.Wrap(err, "db.FetchModelObjects") + } + return ret, nil +} + +func (am *SAppManager) GetApps(providerId string) ([]SApp, error) { + q := AppManager.Query().Equals("manager_id", providerId) + ret := []SApp{} + err := db.FetchModelObjects(AppManager, q, &ret) + if err != nil { + return nil, errors.Wrap(err, "db.FetchModelObjects") + } + return ret, nil +} + +func (self *SCloudregion) GetApps(managerId string) ([]SApp, error) { + q := AppManager.Query().Equals("cloudregion_id", self.Id) + if len(managerId) > 0 { + q = q.Equals("manager_id", managerId) + } + ret := []SApp{} + err := db.FetchModelObjects(AppManager, q, &ret) + if err != nil { + return nil, errors.Wrap(err, "db.FetchModelObjects") + } + return ret, nil +} + +func (self *SCloudregion) SyncApps(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, exts []cloudprovider.ICloudApp) compare.SyncResult { + lockman.LockRawObject(ctx, AppManager.KeywordPlural(), fmt.Sprintf("%s-%s", provider.Id, self.Id)) + defer lockman.ReleaseRawObject(ctx, AppManager.KeywordPlural(), fmt.Sprintf("%s-%s", provider.Id, self.Id)) + result := compare.SyncResult{} + apps, err := self.GetApps(provider.Id) + if err != nil { + result.Error(err) + return result + } + + removed := make([]SApp, 0) + commondb := make([]SApp, 0) + commonext := make([]cloudprovider.ICloudApp, 0) + added := make([]cloudprovider.ICloudApp, 0) + // compare + err = compare.CompareSets(apps, exts, &removed, &commondb, &commonext, &added) + if err != nil { + result.Error(err) + return result + } + + // remove + for i := 0; i < len(removed); i++ { + err := removed[i].syncRemoveCloudApp(ctx, userCred) + if err != nil { + result.DeleteError(err) + continue + } + result.Delete() + } + + // sync with cloud app + for i := 0; i < len(commondb); i++ { + err := commondb[i].SyncWithCloudApp(ctx, userCred, provider, commonext[i]) + if err != nil { + result.UpdateError(err) + continue + } + result.Update() + } + + // new one + for i := 0; i < len(added); i++ { + _, err := self.newFromCloudApp(ctx, userCred, provider, added[i]) + if err != nil { + result.AddError(err) + continue + } + result.Add() + } + return result +} + +func (self *SCloudregion) newFromCloudApp(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, ext cloudprovider.ICloudApp) (*SApp, error) { + app := SApp{} + app.SetModelManager(AppManager, &app) + + app.ExternalId = ext.GetGlobalId() + app.CloudregionId = self.Id + app.ManagerId = provider.Id + app.IsEmulated = ext.IsEmulated() + app.Status = ext.GetStatus() + app.Type = ext.GetType() + app.Kind = ext.GetKind() + app.TechStack = ext.GetTechStack() + app.Name = ext.GetName() + app.Enabled = tristate.True + + err := AppManager.TableSpec().Insert(ctx, &app) + if err != nil { + return nil, errors.Wrap(err, "unable to insert app") + } + aes, err := ext.GetEnvironments() + if err != nil { + return &app, errors.Wrap(err, "unable to GetEnvironments") + } + result := app.SyncAppEnvironments(ctx, userCred, provider, aes) + if result.IsError() { + return &app, errors.Wrap(result.AllError(), "unable to SyncAppEnvironments") + } + SyncCloudProject(userCred, &app, provider.GetOwnerId(), ext, provider.Id) + + db.OpsLog.LogEvent(&app, db.ACT_CREATE, app.GetShortDesc(ctx), userCred) + + return &app, nil +} + +func (am *SAppManager) purgeAll(ctx context.Context, userCred mcclient.TokenCredential, providerId string) error { + apps, err := am.GetApps(providerId) + if err != nil { + return errors.Wrapf(err, "unable to GetApps of provider id %s", providerId) + } + for i := range apps { + err = apps[i].purge(ctx, userCred) + if err != nil { + return err + } + } + return nil +} + +func (a *SApp) purge(ctx context.Context, userCred mcclient.TokenCredential) error { + aes, err := a.GetAppEnvironments() + if err != nil { + return errors.Wrap(err, "unable to GetAppEnvironments") + } + for i := range aes { + err = aes[i].Delete(ctx, userCred) + if err != nil { + return errors.Wrapf(err, "unable to Delete AppEnvironment %s", aes[i].Id) + } + } + return a.Delete(ctx, userCred) +} + +func (a *SApp) syncRemoveCloudApp(ctx context.Context, userCred mcclient.TokenCredential) error { + return a.purge(ctx, userCred) +} + +func (a *SApp) SyncWithCloudApp(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, ext cloudprovider.ICloudApp) error { + _, err := db.UpdateWithLock(ctx, a, func() error { + a.ExternalId = ext.GetGlobalId() + a.Status = ext.GetStatus() + a.Type = ext.GetType() + a.Kind = ext.GetKind() + a.TechStack = ext.GetTechStack() + return nil + }) + if err != nil { + return errors.Wrap(err, "db.Update") + } + aes, err := ext.GetEnvironments() + if err != nil { + return errors.Wrapf(err, "unable to GetAppEnvironments for ICloudApp %s", ext.GetGlobalId()) + } + result := a.SyncAppEnvironments(ctx, userCred, provider, aes) + if result.IsError() { + return result.AllError() + } + return nil +} + +func (am *SAppManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.AppDetails { + rows := make([]api.AppDetails, len(objs)) + virtRows := am.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + manRows := am.SManagedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + regRows := am.SCloudregionResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + for i := range rows { + rows[i].VirtualResourceDetails = virtRows[i] + rows[i].ManagedResourceInfo = manRows[i] + rows[i].CloudregionResourceInfo = regRows[i] + } + return rows +} + +func (a *SApp) AllowPerformSyncstatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return a.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, a, "syncstatus") +} + +func (a *SApp) PerformSyncstatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + var openTask = true + count, err := taskman.TaskManager.QueryTasksOfObject(a, time.Now().Add(-3*time.Minute), &openTask).CountWithError() + if err != nil { + return nil, err + } + if count > 0 { + return nil, httperrors.NewBadRequestError("WebApp has %d task active, can't sync status", count) + } + + return nil, StartResourceSyncStatusTask(ctx, userCred, a, "AppSyncstatusTask", "") +} + +func (a *SApp) GetRegion() (*SCloudregion, error) { + region, err := CloudregionManager.FetchById(a.CloudregionId) + if err != nil { + return nil, errors.Wrapf(err, "CloudregionManager.FetchById(%s)", a.CloudregionId) + } + return region.(*SCloudregion), nil +} + +func (a *SApp) GetIRegion() (cloudprovider.ICloudRegion, error) { + region, err := a.GetRegion() + if err != nil { + return nil, errors.Wrap(err, "GetRegion") + } + provider, err := a.GetDriver() + if err != nil { + return nil, errors.Wrap(err, "GetDriver") + } + return provider.GetIRegionById(region.GetExternalId()) +} + +func (a *SApp) GetIApp() (cloudprovider.ICloudApp, error) { + if len(a.ExternalId) == 0 { + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "empty externalId") + } + iRegion, err := a.GetIRegion() + if err != nil { + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "GetIRegion") + } + return iRegion.GetICloudAppById(a.ExternalId) +} diff --git a/pkg/compute/models/app_environment.go b/pkg/compute/models/app_environment.go new file mode 100644 index 0000000000..84d846858b --- /dev/null +++ b/pkg/compute/models/app_environment.go @@ -0,0 +1,220 @@ +// 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 models + +import ( + "context" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/compare" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +type SAppEnvironmentManager struct { + db.SVirtualResourceBaseManager + db.SExternalizedResourceBaseManager +} + +type SAppEnvironment struct { + db.SVirtualResourceBase + db.SExternalizedResourceBase + + AppId string `width:"36" charset:"ascii" index:"true"` + InstanceType string `width:"16" charset:"ascii" index:"true" get:"user" list:"user"` + InstanceNumber int `get:"user" list:"user"` +} + +var AppEnvironmentManager *SAppEnvironmentManager + +func init() { + AppEnvironmentManager = &SAppEnvironmentManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SAppEnvironment{}, + "appenvironments_tbl", + "webappenvironment", + "webappenvironments", + ), + } + AppEnvironmentManager.SetVirtualObject(AppEnvironmentManager) +} + +func (aem *SAppEnvironmentManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.AppEnvironmentListInput) (*sqlchemy.SQuery, error) { + var err error + q, err = aem.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter") + } + q, err = aem.SExternalizedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.ExternalizedResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SExternalizedResourceBaseManager.ListItemFilter") + } + if input.AppId != "" { + q = q.Equals("app_id", input.AppId) + } + if input.InstanceType != "" { + q = q.Equals("instance_type", input.InstanceType) + } + return q, nil +} + +func (aem *SAppEnvironmentManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.AppEnvironmentListInput) (*sqlchemy.SQuery, error) { + return aem.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput) +} + +func (aem *SAppEnvironmentManager) ListItemExportKeys(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, keys stringutils2.SSortedStrings) (*sqlchemy.SQuery, error) { + var err error + q, err = aem.SVirtualResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemExportKeys") + } + return q, nil +} + +func (aem *SAppEnvironmentManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) { + q, err := aem.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field) + if err == nil { + return q, nil + } + switch field { + case "instance_type": + q = aem.Query("instance_type").Distinct() + } + return q, nil +} + +func (a *SApp) SyncAppEnvironments(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, exts []cloudprovider.ICloudAppEnvironment) compare.SyncResult { + lockman.LockRawObject(ctx, AppEnvironmentManager.KeywordPlural(), a.Id) + defer lockman.LockRawObject(ctx, AppEnvironmentManager.KeywordPlural(), a.Id) + result := compare.SyncResult{} + aes, err := a.GetAppEnvironments() + if err != nil { + result.Error(err) + return result + } + + removed := make([]SAppEnvironment, 0) + commondb := make([]SAppEnvironment, 0) + commonext := make([]cloudprovider.ICloudAppEnvironment, 0) + added := make([]cloudprovider.ICloudAppEnvironment, 0) + // compare + err = compare.CompareSets(aes, exts, &removed, &commondb, &commonext, &added) + if err != nil { + result.Error(err) + return result + } + + // remove + for i := 0; i < len(removed); i++ { + err := removed[i].syncRemoveCloudAppEnvironment(ctx, userCred) + if err != nil { + result.DeleteError(err) + continue + } + result.Delete() + } + + // sync with cloud + for i := 0; i < len(commondb); i++ { + err := commondb[i].SyncWithCloudAppEnvironment(ctx, userCred, commonext[i]) + if err != nil { + result.UpdateError(err) + continue + } + result.Update() + } + + // new one + for i := 0; i < len(added); i++ { + _, err := a.newFromCloudAppEnvironment(ctx, userCred, provider, added[i]) + if err != nil { + result.AddError(err) + continue + } + result.Add() + } + return result +} + +func (a *SApp) newFromCloudAppEnvironment(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, ext cloudprovider.ICloudAppEnvironment) (*SAppEnvironment, error) { + var err error + appEnvironment := SAppEnvironment{} + appEnvironment.SetModelManager(AppEnvironmentManager, &appEnvironment) + + appEnvironment.ExternalId = ext.GetGlobalId() + appEnvironment.IsEmulated = ext.IsEmulated() + appEnvironment.Status = ext.GetStatus() + appEnvironment.InstanceType, err = ext.GetInstanceType() + if err != nil { + return nil, errors.Wrap(err, "unable to GetInsntaceType") + } + + appEnvironment.InstanceNumber, err = ext.GetInstanceNumber() + if err != nil { + return nil, errors.Wrap(err, "unable to GetInstanceNumber") + } + + appEnvironment.Name = ext.GetName() + appEnvironment.AppId = a.Id + err = AppEnvironmentManager.TableSpec().Insert(ctx, &appEnvironment) + if err != nil { + return nil, errors.Wrapf(err, "newFromCloudAppEnvironment.Insert") + } + + SyncCloudProject(userCred, &appEnvironment, provider.GetOwnerId(), ext, provider.Id) + db.OpsLog.LogEvent(&appEnvironment, db.ACT_CREATE, appEnvironment.GetShortDesc(ctx), userCred) + return nil, nil +} + +func (ae *SAppEnvironment) syncRemoveCloudAppEnvironment(ctx context.Context, userCred mcclient.TokenCredential) error { + return ae.Delete(ctx, userCred) +} + +func (ae *SAppEnvironment) SyncWithCloudAppEnvironment(ctx context.Context, userCred mcclient.TokenCredential, ext cloudprovider.ICloudAppEnvironment) error { + _, err := db.UpdateWithLock(ctx, ae, func() error { + var err error + ae.ExternalId = ext.GetGlobalId() + ae.Status = ext.GetStatus() + ae.InstanceType, err = ext.GetInstanceType() + if err != nil { + return err + } + ae.InstanceNumber, err = ext.GetInstanceNumber() + if err != nil { + return err + } + return nil + }) + if err != nil { + return errors.Wrap(err, "db.Update") + } + return nil +} + +func (aem *SAppEnvironmentManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.AppEnvironmentDetails { + rows := make([]api.AppEnvironmentDetails, len(objs)) + virRows := aem.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + for i := range rows { + rows[i].VirtualResourceDetails = virRows[i] + } + return rows +} diff --git a/pkg/compute/models/cloudproviders.go b/pkg/compute/models/cloudproviders.go index 95031f8976..3f8b4307d2 100644 --- a/pkg/compute/models/cloudproviders.go +++ b/pkg/compute/models/cloudproviders.go @@ -1484,6 +1484,7 @@ func (self *SCloudprovider) RealDelete(ctx context.Context, userCred mcclient.To WafIPSetCacheManager, WafRegexSetCacheManager, WafInstanceManager, + AppManager, VpcManager, ElasticipManager, MongoDBManager, diff --git a/pkg/compute/models/cloudsync.go b/pkg/compute/models/cloudsync.go index bf91cf9d47..e0701bc8ae 100644 --- a/pkg/compute/models/cloudsync.go +++ b/pkg/compute/models/cloudsync.go @@ -1170,6 +1170,24 @@ func syncKafkas(ctx context.Context, userCred mcclient.TokenCredential, syncResu return nil } +func syncApps(ctx context.Context, userCred mcclient.TokenCredential, syncResults SSyncResultSet, provider *SCloudprovider, localRegion *SCloudregion, remoteRegion cloudprovider.ICloudRegion) error { + log.Infof("try to syncApps") + iApps, err := remoteRegion.GetICloudApps() + if err != nil { + msg := fmt.Sprintf("GetICloudApps for region %s failed %s", remoteRegion.GetName(), err) + log.Errorf(msg) + return err + } + result := localRegion.SyncApps(ctx, userCred, provider, iApps) + syncResults.Add(AppManager, result) + msg := result.Result() + log.Infof("SyncApps for region %s result: %s", localRegion.Name, msg) + if result.IsError() { + return result.AllError() + } + return nil +} + func syncWafInstances(ctx context.Context, userCred mcclient.TokenCredential, syncResults SSyncResultSet, provider *SCloudprovider, localRegion *SCloudregion, remoteRegion cloudprovider.ICloudRegion) error { wafIns, err := remoteRegion.GetICloudWafInstances() if err != nil { @@ -1416,6 +1434,10 @@ func syncPublicCloudProviderInfo( syncKafkas(ctx, userCred, syncResults, provider, localRegion, remoteRegion) } + if utils.IsInStringArray(cloudprovider.CLOUD_CAPABILITY_APP, driver.GetCapabilities()) { + syncApps(ctx, userCred, syncResults, provider, localRegion, remoteRegion) + } + if cloudprovider.IsSupportCompute(driver) { log.Debugf("storageCachePairs count %d", len(storageCachePairs)) for i := range storageCachePairs { diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index 5f66d04867..84b0bbd30d 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -231,6 +231,9 @@ func InitHandlers(app *appsrv.Application) { models.ElasticSearchManager, models.KafkaManager, + + models.AppManager, + models.AppEnvironmentManager, } { db.RegisterModelManager(manager) handler := db.NewModelHandler(manager) diff --git a/pkg/compute/tasks/app_syncstatus_task.go b/pkg/compute/tasks/app_syncstatus_task.go new file mode 100644 index 0000000000..2a70ef78aa --- /dev/null +++ b/pkg/compute/tasks/app_syncstatus_task.go @@ -0,0 +1,55 @@ +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type AppSyncstatusTask struct { + taskman.STask +} + +func init() { + taskman.RegisterTask(AppSyncstatusTask{}) +} + +func (at *AppSyncstatusTask) taskFailed(ctx context.Context, app *models.SApp, err error) { + app.SetStatus(at.UserCred, api.APP_STATUS_UNKNOWN, err.Error()) + db.OpsLog.LogEvent(app, db.ACT_SYNC_STATUS, err, at.GetUserCred()) + logclient.AddActionLogWithStartable(at, app, logclient.ACT_SYNC_STATUS, err, at.UserCred, false) + at.SetStageFailed(ctx, jsonutils.NewString(err.Error())) +} + +func (at *AppSyncstatusTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + app := obj.(*models.SApp) + + iApp, err := app.GetIApp() + if err != nil { + at.taskFailed(ctx, app, errors.Wrapf(err, "app.GetIApp")) + return + } + app.SyncWithCloudApp(ctx, at.UserCred, app.GetCloudprovider(), iApp) + at.SetStageComplete(ctx, nil) +} diff --git a/pkg/mcclient/modules/mod_app.go b/pkg/mcclient/modules/mod_app.go new file mode 100644 index 0000000000..315be54579 --- /dev/null +++ b/pkg/mcclient/modules/mod_app.go @@ -0,0 +1,43 @@ +// 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 modules + +import "yunion.io/x/onecloud/pkg/mcclient/modulebase" + +type AppManager struct { + modulebase.ResourceManager +} + +type AppEnvironmentManager struct { + modulebase.ResourceManager +} + +var ( + Apps AppManager + AppEnvironments AppEnvironmentManager +) + +func init() { + Apps = AppManager{NewComputeManager("webapp", "webapps", + []string{}, + []string{}, + )} + AppEnvironments = AppEnvironmentManager{NewComputeManager("webappenvironment", "webappenvironments", + []string{}, + []string{}, + )} + registerCompute(&Apps) + registerCompute(&AppEnvironments) +} diff --git a/pkg/mcclient/options/compute/app.go b/pkg/mcclient/options/compute/app.go new file mode 100644 index 0000000000..bfb077683b --- /dev/null +++ b/pkg/mcclient/options/compute/app.go @@ -0,0 +1,64 @@ +// 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 AppListOptions struct { + options.BaseListOptions + TechStack string +} + +func (opts *AppListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(opts) +} + +type AppIdOptions struct { + ID string `help:"App Id"` +} + +func (opts *AppIdOptions) GetId() string { + return opts.ID +} + +func (opts *AppIdOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil +} + +type AppEnvironmentListOptions struct { + options.BaseListOptions + AppId string `help:"App Id"` + InstanceType string `help:"Instance Type"` +} + +func (opts *AppEnvironmentListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(opts) +} + +type AppEnvironmentIdOption struct { + ID string `help:"AppEnvironment ID"` +} + +func (opts *AppEnvironmentIdOption) GetId() string { + return opts.ID +} + +func (opts *AppEnvironmentIdOption) Params() (jsonutils.JSONObject, error) { + return nil, nil +} diff --git a/pkg/multicloud/azure/app_service_plan.go b/pkg/multicloud/azure/app_service_plan.go new file mode 100644 index 0000000000..dcb6940e2a --- /dev/null +++ b/pkg/multicloud/azure/app_service_plan.go @@ -0,0 +1,199 @@ +// 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 azure + +import ( + "fmt" + "net/url" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/multicloud" +) + +type SAppServicePlan struct { + multicloud.AzureTags + region *SRegion + + Properties SAppServicePlanProperties + ID string + Kind string + Location string + Name string + Type string + Sku SSkuDescription +} + +type SAppServicePlanProperties struct { + FreeOfferExpirationTime string + GeoRegion string + HostingEnvironmentProfile SHostingEnvironmentProfile + HyperV bool + IsSpot bool + MaximumElasticWorkerCount int + MaximumNumberOfWorkers int + NumberOfSites int + PerSiteScaling bool + // 应用服务环境的预配状态 + ProvisioningState string + Reserved bool + ResourceGroup string + SpotExpirationTime string + // Creating Pending Ready + Status string + Subscription string + TargetWorkerCount int + TargetWorkerSizeId int + WorkerTierName string +} + +type SHostingEnvironmentProfile struct { + ID string + Name string + Type string +} + +type SSkuDescription struct { + Capabilities []SCapability + Capacity int + Family string + locations []string + Name string + Size string + SkuCapacity SSkuCapacity + Tier string +} + +type SCapability struct { + Name string + Reason string + Value string +} + +type SSkuCapacity struct { + Default int + Maximum int + Minimum int + ScaleType string +} + +func (r *SRegion) GetAppServicePlanWithCache(id string) (*SAppServicePlan, error) { + if r.appServicePlans == nil { + plans, err := r.GetAppServicePlans() + if err != nil { + return nil, err + } + appServicePlans := make(map[string]*SAppServicePlan, len(plans)) + for i := range plans { + appServicePlans[plans[i].ID] = &plans[i] + } + r.appServicePlans = appServicePlans + } + if ret, ok := r.appServicePlans[id]; ok { + return ret, nil + } + return r.GetAppServicePlan(id) +} + +func (r *SRegion) GetAppServicePlans() ([]SAppServicePlan, error) { + result := []SAppServicePlan{} + resource := "Microsoft.Web/serverfarms" + err := r.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + for i := range result { + result[i].region = r + } + return result, nil +} + +func (r *SRegion) GetAppServicePlan(planId string) (*SAppServicePlan, error) { + asp := &SAppServicePlan{region: r} + params := url.Values{"api-version": []string{"2019-08-01"}} + return asp, r.get(planId, params, asp) +} + +type SServerFarmSku struct { + ResourceType string + Sku SSkuSpec + Capacity SSkuCapacity +} + +type SSkuSpec struct { + Name string + Tier string +} + +func (asp *SAppServicePlan) GetSkus() ([]SServerFarmSku, error) { + result := []SServerFarmSku{} + resource := fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/serverfarms/%s/skus", asp.region.client._subscriptionId(), asp.Properties.ResourceGroup, asp.Name) + err := asp.region.client.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + return result, nil +} + +type SAppServicePlanVnet struct { + Id string + Name string + Properties SASPVnetProperties +} + +type SASPVnetProperties struct { + VnetResourceId string +} + +func (asp *SAppServicePlan) GetNetworks() ([]SNetwork, error) { + vnets, err := asp.GetVnets() + if err != nil { + return nil, errors.Wrap(err, "unable to get Vnets") + } + networks := make([]SNetwork, 0, len(vnets)) + for i := range vnets { + network, err := asp.region.GetNetwork(vnets[i].Properties.VnetResourceId) + if err != nil { + return nil, errors.Wrap(err, "unable to get Network") + } + networks = append(networks, *network) + } + return networks, nil +} + +func (asp *SAppServicePlan) GetVnets() ([]SAppServicePlanVnet, error) { + result := []SAppServicePlanVnet{} + resource := fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/serverfarms/%s/virtualNetworkConnections", asp.region.client._subscriptionId(), asp.Properties.ResourceGroup, asp.Name) + err := asp.region.client.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + return result, nil +} + +type SSkuInfos struct { + ResourceType string + Skus []SResourceSku +} + +func (r *SRegion) GetAppServiceSkus() ([]SSkuInfos, error) { + result := []SSkuInfos{} + resource := "Microsoft.Web/skus" + err := r.client.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + return result, err +} diff --git a/pkg/multicloud/azure/autoscale_setting.go b/pkg/multicloud/azure/autoscale_setting.go new file mode 100644 index 0000000000..afbd725871 --- /dev/null +++ b/pkg/multicloud/azure/autoscale_setting.go @@ -0,0 +1,156 @@ +// 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 azure + +import ( + "net/url" + + "yunion.io/x/onecloud/pkg/multicloud" +) + +type SAutoscaleSettingResource struct { + multicloud.AzureTags + region *SRegion + + Properties SAutoscaleSettingProperties + ID string + Location string + Name string + Type string +} + +type SAutoscaleSettingProperties struct { + Enabled bool + Name string + TargetResourceUri string + Profiles []SAutoscaleProfile +} + +type SAutoscaleProfile struct { + Capacity SScaleCapacity + FixedDate STimeWindow + Name string + Recurrence SRecurrence + Rule []SScaleRule +} + +type SRecurrence struct { + Frequency string + Schedule SRecurrentSchedule +} + +type SRecurrentSchedule struct { + Days []string + Hours []int + Minutes []int + TimeZone string +} + +type STimeWindow struct { + // 采用 ISO 8601 格式的配置文件的结束时间。 + End string + // 采用 ISO 8601 格式的配置文件的开始时间。 + Start string + TimeZone string +} + +type SScaleAction struct { + Cooldown string + Direction string + Type string + Value string +} + +// 可以在此配置文件期间使用的实例数。 +type SScaleCapacity struct { + Default string + Maximum string + Minimum string +} + +// Decrease string +// Increase string +// None string + +// 为缩放操作提供触发器和参数的规则。 +type SScaleRule struct { + MetricTrigger SMetricTrigger + ScaleAction SScaleAction +} + +// 维度运算符。 仅支持"Equals"和"NotEquals"。 "Equals"等于任何值。 "NotEquals"不等于所有值 +type SScaleRuleMetricDimensionOperationType struct { + Equals string + NotEquals string +} + +//触发缩放规则时应发生的操作类型。 +// ChangeCount string +// ExactCount string +// PercentChangeCount string + +type SScaleRuleMetricDimension struct { + DimensionName string + Operator string + Values []string +} + +type SMetricTrigger struct { + // 维度条件的列表。 例如: [{"DimensionName": "AppName"、"Operator": "Equals"、"Values": ["App1"]}、{"DimensionName": "Deployment"、"Operator": "Equals"、"Values": ["default"]}]。 + Dimensions []SScaleRuleMetricDimension + + // 一个值,该值指示度量值是否应除以每个实例。 + DividePerInstance bool + + // 定义规则监视内容的指标的名称。 + MetricName string + + // 定义规则监视内容的指标的命名空间。 + MetricNamespace string + + // 规则监视的资源的资源标识符。 + MetricResourceUri string + + // 用于比较指标数据和阈值的运算符。 + // Equals GreaterThan GreaterThanOrEqual LessThan LessThanOrEqual NotEquals + Operator string + + // 指标统计信息类型。 来自多个实例的指标进行组合的方式。 + // Average Max Min Sum + Statistic string + + // 触发缩放操作的度量值的阈值。 + Threshold int + + // 时间聚合类型。 随着时间推移,收集的数据应如何组合。 默认值为 Average。 + // Average Count Last Maximum Minimum Total + TimeAggregation string + + //规则监视的度量值的粒度。 必须是从指标的指标定义返回的预定义值之一。 必须介于 12 小时和 1 分钟之间。 + TimeGrain string + + //收集实例数据的时间范围。 此值必须大于指标集合中的延迟,可能会因资源而异。 必须介于 12 小时和 5 分钟之间。 + TimeWindow string +} + +func (r *SRegion) GetAutoscaleSettingResources() ([]SAutoscaleSettingResource, error) { + result := []SAutoscaleSettingResource{} + resource := "microsoft.insights/autoscalesettings" + err := r.client.list(resource, url.Values{"api-version": []string{"2015-04-01"}}, &result) + if err != nil { + return nil, err + } + return result, nil +} diff --git a/pkg/multicloud/azure/azure.go b/pkg/multicloud/azure/azure.go index 82d5be4307..aec0080a01 100644 --- a/pkg/multicloud/azure/azure.go +++ b/pkg/multicloud/azure/azure.go @@ -508,10 +508,14 @@ func (self *SAzureClient) _list(resource string, params url.Values) (jsonutils.J path = fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/%s", subId, params.Get("resourceGroups"), resource) params.Del("resourceGroups") default: - if len(subId) == 0 { - return nil, fmt.Errorf("no avaiable subscriptions") + if strings.HasPrefix(resource, "subscriptions/") || strings.HasPrefix(resource, "/subscriptions/") { + path = resource + } else { + if len(subId) == 0 { + return nil, fmt.Errorf("no avaiable subscriptions") + } + path = fmt.Sprintf("subscriptions/%s/providers/%s", subId, resource) } - path = fmt.Sprintf("subscriptions/%s/providers/%s", subId, resource) } params.Set("api-version", self._apiVersion(resource, params)) return self.jsonRequest("GET", path, nil, params, true) @@ -976,6 +980,7 @@ func (self *SAzureClient) GetCapabilities() []string { cloudprovider.CLOUD_CAPABILITY_SAML_AUTH, cloudprovider.CLOUD_CAPABILITY_WAF, cloudprovider.CLOUD_CAPABILITY_CACHE, + cloudprovider.CLOUD_CAPABILITY_APP, } return caps } diff --git a/pkg/multicloud/azure/region.go b/pkg/multicloud/azure/region.go index 0ca8257da6..ec428af2f1 100644 --- a/pkg/multicloud/azure/region.go +++ b/pkg/multicloud/azure/region.go @@ -42,7 +42,8 @@ type SRegion struct { multicloud.SRegion client *SAzureClient - storageCache *SStoragecache + storageCache *SStoragecache + appServicePlans map[string]*SAppServicePlan ID string Name string diff --git a/pkg/multicloud/azure/shell/app_service_plan.go b/pkg/multicloud/azure/shell/app_service_plan.go new file mode 100644 index 0000000000..bb8fe095c6 --- /dev/null +++ b/pkg/multicloud/azure/shell/app_service_plan.go @@ -0,0 +1,81 @@ +// 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 shell + +import ( + "fmt" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/multicloud/azure" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type AppServicePlanListOptions struct { + } + shellutils.R(&AppServicePlanListOptions{}, "app-service-plan-list", "List app service plan", func(cli *azure.SRegion, args *AppServicePlanListOptions) error { + appServicePlans, err := cli.GetAppServicePlans() + if err != nil { + return err + } + fmt.Printf("sku: %s\n", jsonutils.Marshal(appServicePlans[0].Sku)) + printList(appServicePlans, len(appServicePlans), 0, 0, []string{}) + return nil + }) + type AppServicePlanShowOptions struct { + ID string + } + shellutils.R(&AppServicePlanShowOptions{}, "app-service-plan-show", "Show app service plan", func(cli *azure.SRegion, args *AppServicePlanShowOptions) error { + appServicePlan, err := cli.GetAppServicePlan(args.ID) + if err != nil { + return err + } + printObject(appServicePlan) + return nil + }) + shellutils.R(&AppServicePlanListOptions{}, "autoscale-setting-list", "List autoscale setting in app service plan", func(cli *azure.SRegion, args *AppServicePlanListOptions) error { + appServicePlans, err := cli.GetAutoscaleSettingResources() + if err != nil { + return err + } + printList(appServicePlans, len(appServicePlans), 0, 0, []string{}) + return nil + }) + shellutils.R(&AppServicePlanShowOptions{}, "app-service-sku-list", "List sku usable in app service plan", func(cli *azure.SRegion, args *AppServicePlanShowOptions) error { + appServicePlan, err := cli.GetAppServicePlan(args.ID) + if err != nil { + return err + } + skus, err := appServicePlan.GetSkus() + if err != nil { + return err + } + printList(skus, len(skus), 0, 0, []string{}) + return nil + }) + shellutils.R(&AppServicePlanShowOptions{}, "app-service-network-list", "List network connected with app service plan", func(cli *azure.SRegion, args *AppServicePlanShowOptions) error { + appServicePlan, err := cli.GetAppServicePlan(args.ID) + if err != nil { + return err + } + networks, err := appServicePlan.GetNetworks() + if err != nil { + return err + } + printList(networks, len(networks), 0, 0, []string{}) + return nil + }) +} diff --git a/pkg/multicloud/azure/shell/web_app.go b/pkg/multicloud/azure/shell/web_app.go new file mode 100644 index 0000000000..912bb97a4e --- /dev/null +++ b/pkg/multicloud/azure/shell/web_app.go @@ -0,0 +1,68 @@ +// 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 shell + +import ( + "yunion.io/x/onecloud/pkg/multicloud/azure" + "yunion.io/x/onecloud/pkg/util/shellutils" +) + +func init() { + type AppSiteListOptions struct { + } + shellutils.R(&AppSiteListOptions{}, "app-site-list", "List app service plan", func(cli *azure.SRegion, args *AppSiteListOptions) error { + ass, err := cli.GetAppSites() + if err != nil { + return err + } + printList(ass, len(ass), 0, 0, []string{}) + return nil + }) + type AppSiteShowOptions struct { + ID string + } + shellutils.R(&AppSiteShowOptions{}, "app-site-show", "Show app service plan", func(cli *azure.SRegion, args *AppSiteShowOptions) error { + as, err := cli.GetAppSite(args.ID) + if err != nil { + return err + } + printObject(as) + return nil + }) + shellutils.R(&AppSiteShowOptions{}, "app-site-deployment-list", "List sku usable in app service plan", func(cli *azure.SRegion, args *AppSiteShowOptions) error { + as, err := cli.GetAppSite(args.ID) + if err != nil { + return err + } + deployments, err := as.GetDeployments() + if err != nil { + return err + } + printList(deployments, len(deployments), 0, 0, []string{}) + return nil + }) + shellutils.R(&AppSiteShowOptions{}, "app-site-slot-list", "List slots ofr App site", func(cli *azure.SRegion, args *AppSiteShowOptions) error { + as, err := cli.GetAppSite(args.ID) + if err != nil { + return err + } + slots, err := as.GetSlots() + if err != nil { + return err + } + printList(slots, len(slots), 0, 0, []string{}) + return nil + }) +} diff --git a/pkg/multicloud/azure/web_app.go b/pkg/multicloud/azure/web_app.go new file mode 100644 index 0000000000..0802ede373 --- /dev/null +++ b/pkg/multicloud/azure/web_app.go @@ -0,0 +1,302 @@ +// 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 azure + +import ( + "fmt" + "net/url" + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud" +) + +type SAppSite struct { + multicloud.AzureTags + region *SRegion + appServicePlan *SAppServicePlan + + Properties SAppSiteProperties + Id string + Name string + Kind string + Location string + Type string + + stack *string +} + +type SAppSiteProperties struct { + AvailabilityState string + DefaultHostName string + Enabled bool + EnabledHostNames []string + HostNameSslStates []SHostNameSslState + HostNames []string + HttpsOnly bool + OutboundIpAddresses string + ResourceGroup string + ServerFarmId string + SiteConfig SSiteConfig + State string +} + +type SSiteConfig struct { + // TODO +} + +type SHostNameSslState struct { + HostType string + Name string + SslState string + Thumbprint string + ToUpdate bool + VirtualIP string +} + +func (r *SRegion) GetAppSites() ([]SAppSite, error) { + result := []SAppSite{} + resource := "Microsoft.Web/sites" + err := r.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + for i := range result { + result[i].region = r + } + return result, nil +} + +func (r *SRegion) GetICloudApps() ([]cloudprovider.ICloudApp, error) { + ass, err := r.GetAppSites() + if err != nil { + return nil, err + } + apps := make([]cloudprovider.ICloudApp, 0, len(ass)) + for i := range ass { + apps = append(apps, &SApp{ + SAppSite: ass[i], + }) + } + return apps, nil +} + +func (r *SRegion) GetICloudAppById(id string) (cloudprovider.ICloudApp, error) { + as, err := r.GetAppSite(id) + if err != nil { + return nil, err + } + return &SApp{ + SAppSite: *as, + }, nil +} + +func (as *SAppSite) GetSlots() ([]SAppSite, error) { + result := []SAppSite{} + resource := fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/sites/%s/slots", as.region.client.subscriptionId, as.Properties.ResourceGroup, as.Name) + err := as.region.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + return result, nil +} + +func (r *SRegion) GetAppSite(siteId string) (*SAppSite, error) { + as := &SAppSite{region: r} + params := url.Values{"api-version": []string{"2019-08-01"}} + return as, r.get(siteId, params, as) +} + +func (as *SAppSite) GetAppServicePlan() (*SAppServicePlan, error) { + if as.appServicePlan != nil { + return as.appServicePlan, nil + } + plan, err := as.region.GetAppServicePlanWithCache(as.Properties.ServerFarmId) + if err != nil { + return nil, errors.Wrap(err, "unable to get AppServicePlan") + } + as.appServicePlan = plan + return plan, nil +} + +type SDeployment struct { + ID string + Name string + Kind string + Type string + Properties SDeploymentProperties +} + +type SDeploymentProperties struct { + Active bool + Author string + AuthorEmail string + Deployer string + Details string + EndTime string + Message string + StartTime string + Status string +} + +func (as *SAppSite) GetDeployments() ([]SDeployment, error) { + result := []SDeployment{} + resource := fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/sites/%s/deployments", as.region.client._subscriptionId(), as.Properties.ResourceGroup, as.Name) + err := as.region.client.list(resource, url.Values{"api-version": []string{"2019-08-01"}}, &result) + if err != nil { + return nil, err + } + return result, nil +} + +type SStringDictionary struct { + Id string + Kind string + Name string + Type string + Properties map[string]string +} + +func (as *SAppSite) GetStack() (string, error) { + if as.stack != nil { + return *as.stack, nil + } + // result := SStringDictionary{} + resource := fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/sites/%s/config/metadata", as.region.client.subscriptionId, as.Properties.ResourceGroup, as.Name) + + params := url.Values{} + params.Set("api-version", "2019-08-01") + path := resource + "/list" + response, err := as.region.client.jsonRequest("POST", path, nil, params, true) + if err != nil { + return "", err + } + stack, err := response.GetString("properties", "CURRENT_STACK") + if err != nil { + return "", errors.Wrapf(err, "unable to parse response correctly, response: %s", response) + } + as.stack = &stack + return stack, nil +} + +func (as *SAppSite) GetId() string { + return as.Id +} + +func (as *SAppSite) GetName() string { + return as.Name +} + +func (as *SAppSite) GetGlobalId() string { + return strings.ToLower(as.Id) +} + +func (as *SAppSite) GetStatus() string { + return "ready" +} + +func (as *SAppSite) GetProjectId() string { + return strings.ToLower(as.Properties.ResourceGroup) +} + +type SApp struct { + multicloud.SResourceBase + multicloud.AzureTags + SAppSite +} + +type SAppEnvironment struct { + multicloud.SResourceBase + multicloud.AzureTags + SAppSite +} + +func (ae *SAppEnvironment) GetInstanceType() (string, error) { + asp, err := ae.SAppSite.GetAppServicePlan() + if err != nil { + return "", err + } + return asp.Sku.Name, nil +} + +func (ae *SAppEnvironment) GetInstanceNumber() (int, error) { + asp, err := ae.SAppSite.GetAppServicePlan() + if err != nil { + return 0, err + } + return asp.Sku.Capacity, nil +} + +func (a *SApp) GetEnvironments() ([]cloudprovider.ICloudAppEnvironment, error) { + sites, err := a.SAppSite.GetSlots() + if err != nil { + return nil, err + } + aes := make([]cloudprovider.ICloudAppEnvironment, 0, len(sites)+1) + aes = append(aes, &SAppEnvironment{ + SAppSite: a.SAppSite, + }) + for i := range sites { + sites[i].region = a.region + aes = append(aes, &SAppEnvironment{ + SAppSite: sites[i], + }) + } + return aes, nil +} + +var techStacks = map[string]string{ + "dotnet": ".NET", + "dotnetcore": ".NET Core", + "aspdotnet": "ASP.NET", + "node": "Node", + "python": "Python", + "php": "PHP", + "ruby": "Ruby", + "java": "Java", + "javacontainers": "Jave Containers", +} + +func (a *SApp) GetTechStack() string { + if strings.Contains(a.SAppSite.Kind, "container") { + return "Docker container" + } + stack, err := a.GetStack() + if err != nil { + log.Errorf("unable to GetStack: %v", err) + } + if s, ok := techStacks[stack]; ok { + return s + } + return stack +} + +func (a *SApp) GetType() string { + return "web" +} + +func (a *SApp) GetKind() string { + return a.Kind +} + +func (a *SApp) GetOsType() string { + if strings.Contains(a.Kind, "linux") { + return "Linux" + } + return "Windows" +} diff --git a/pkg/multicloud/region_base.go b/pkg/multicloud/region_base.go index 784bd2c02c..d6669c4c84 100644 --- a/pkg/multicloud/region_base.go +++ b/pkg/multicloud/region_base.go @@ -222,3 +222,11 @@ func (self *SRegion) GetICloudKafkas() ([]cloudprovider.ICloudKafka, error) { func (self *SRegion) GetICloudKafkaById(id string) (cloudprovider.ICloudKafka, error) { return nil, errors.Wrapf(cloudprovider.ErrNotImplemented, "GetICloudKafkaById") } + +func (self *SRegion) GetICloudApps() ([]cloudprovider.ICloudApp, error) { + return nil, errors.Wrapf(cloudprovider.ErrNotImplemented, "GetICloudApps") +} + +func (self *SRegion) GetICloudAppById(id string) (cloudprovider.ICloudApp, error) { + return nil, errors.Wrapf(cloudprovider.ErrNotImplemented, "GetICloudAppById") +}