diff --git a/cmd/climc/shell/guest_template.go b/cmd/climc/shell/guest_template.go new file mode 100644 index 0000000000..d3203b6b2b --- /dev/null +++ b/cmd/climc/shell/guest_template.go @@ -0,0 +1,168 @@ +// 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/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + + type GuestTemplateListOptions struct { + options.BaseListOptions + } + + R(&GuestTemplateListOptions{}, "server-template-list", "List server template", func(s *mcclient.ClientSession, + opts *GuestTemplateListOptions) error { + + params, err := options.ListStructToParams(opts) + if err != nil { + return err + } + result, err := modules.GuestTemplate.List(s, params) + if err != nil { + return err + } + printList(result, modules.GuestTemplate.GetColumns(s)) + return nil + }) + + type GuestTemplateCreateOptions struct { + options.ServerCreateOptionalOptions + NAME string `help:"Name of server template" json:"-"` + } + + R(&GuestTemplateCreateOptions{}, "server-template-create", "Create a server template", + func(s *mcclient.ClientSession, + opts *GuestTemplateCreateOptions) error { + + params, err := opts.OptionalParams() + if err != nil { + return err + } + if options.BoolV(opts.DryRun) { + fmt.Println("no support operator") + return nil + } + + dict := jsonutils.NewDict() + if opts.GenerateName { + dict.Add(jsonutils.NewString(opts.NAME), "generate_name") + } else { + dict.Add(jsonutils.NewString(opts.NAME), "name") + } + dict.Add(params.JSON(params), "content") + tem, err := modules.GuestTemplate.Create(s, dict) + if err != nil { + return err + } + printObject(tem) + return nil + }) + + type GuestTemplateUpdateOptions struct { + options.ServerCreateOptionalOptions + ID string `help:"ID of server template"` + name string `help:"name of server template"` + } + + R(&GuestTemplateUpdateOptions{}, "server-template-update", "Update a server template", + func(s *mcclient.ClientSession, opts *GuestTemplateUpdateOptions) error { + + params, err := opts.OptionalParams() + if err != nil { + return err + } + if options.BoolV(opts.DryRun) { + fmt.Println("no support operator") + return nil + } + dict := jsonutils.NewDict() + if len(opts.name) != 0 { + dict.Add(jsonutils.NewString(opts.name), "name") + } + dict.Add(params.JSON(params), "content") + tem, err := modules.GuestTemplate.Update(s, opts.ID, dict) + if err != nil { + return err + } + printObject(tem) + return nil + }) + + type GuestTemplateOptions struct { + ID string `help:"ID or Name of server template"` + } + + R(&GuestTemplateOptions{}, "server-template-show", "Show a server template", + func(s *mcclient.ClientSession, opts *GuestTemplateOptions) error { + tem, err := modules.GuestTemplate.Get(s, opts.ID, jsonutils.JSONNull) + if err != nil { + return err + } + printObject(tem) + return nil + }) + + R(&GuestTemplateOptions{}, "server-tempalte-delete", "Delete a server template", + func(s *mcclient.ClientSession, opts *GuestTemplateOptions) error { + + tem, err := modules.GuestTemplate.Delete(s, opts.ID, jsonutils.JSONNull) + if err != nil { + return err + } + printObject(tem) + return nil + }, + ) + + R(&GuestTemplateOptions{}, "server-template-private", "Private server template", + func(s *mcclient.ClientSession, opts *GuestTemplateOptions) error { + tem, err := modules.GuestTemplate.PerformAction(s, opts.ID, "private", jsonutils.JSONNull) + if err != nil { + return err + } + printObject(tem) + return nil + }, + ) + + type GuestTemplatePublicOptions struct { + ID string `help:"ID or Name of server template"` + PublicScope string `help:"public scope"` + } + + R(&GuestTemplatePublicOptions{}, "server-template-public", "Public server template", + func(s *mcclient.ClientSession, opts *GuestTemplatePublicOptions) error { + + dict := jsonutils.NewDict() + if len(opts.PublicScope) != 0 { + dict.Add(jsonutils.NewString(opts.PublicScope), "public_scope") + } + tem, err := modules.GuestTemplate.PerformAction(s, opts.ID, "public", dict) + if err != nil { + return err + } + printObject(tem) + return nil + }, + ) +} diff --git a/cmd/climc/shell/service_catalog.go b/cmd/climc/shell/service_catalog.go new file mode 100644 index 0000000000..f604dafc95 --- /dev/null +++ b/cmd/climc/shell/service_catalog.go @@ -0,0 +1,162 @@ +// 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/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + type ServiceCatalogListOpions struct { + options.BaseListOptions + } + + R(&ServiceCatalogListOpions{}, "service-catalog-list", "List service catalog", func(s *mcclient.ClientSession, + opts *ServiceCatalogListOpions) error { + + params, err := options.ListStructToParams(opts) + if err != nil { + return err + } + ret, err := modules.ServiceCatalog.List(s, params) + if err != nil { + return err + } + printList(ret, modules.ServiceCatalog.GetColumns(s)) + return nil + }) + + type ServiceCatalogCreateOptions struct { + NAME string `help:"Name of service catalog"` + GuestTemplate string `help:"guest template of service catalog"` + IconUrl string `help:"icon url of service catalog"` + GenerateName bool `help:"whether to generate name"` + } + + R(&ServiceCatalogCreateOptions{}, "service-catalog-create", "Create a service catalog", + func(s *mcclient.ClientSession, opts *ServiceCatalogCreateOptions) error { + + params := jsonutils.NewDict() + if opts.GenerateName { + params.Add(jsonutils.NewString(opts.NAME), "generate_name") + } else { + params.Add(jsonutils.NewString(opts.NAME), "name") + } + if len(opts.GuestTemplate) != 0 { + params.Add(jsonutils.NewString(opts.GuestTemplate), "guest_template") + } + if len(opts.IconUrl) != 0 { + params.Add(jsonutils.NewString(opts.IconUrl), "icon_url") + } + serviceCatalog, err := modules.ServiceCatalog.Create(s, params) + if err != nil { + return err + } + printObject(serviceCatalog) + return nil + }, + ) + + type ServiceCatalogOptions struct { + ID string `help:"ID or name of service catalog"` + } + + R(&ServiceCatalogOptions{}, "service-catalog-show", "show a service catalog", func(s *mcclient.ClientSession, + opts *ServiceCatalogOptions) error { + + sc, err := modules.ServiceCatalog.Get(s, opts.ID, jsonutils.JSONNull) + if err != nil { + return err + } + printObject(sc) + return nil + }) + + R(&ServiceCatalogOptions{}, "service-catalog-delete", "delete a service catalog", func(s *mcclient.ClientSession, + opts *ServiceCatalogOptions) error { + + sc, err := modules.ServiceCatalog.Delete(s, opts.ID, jsonutils.JSONNull) + if err != nil { + return err + } + printObject(sc) + return nil + }) + + type ServiceCatalogUpdateOptions struct { + ServiceCatalogOptions + Name string `help:"Name of service catalog"` + GuestTemplate string `help:"guest template of service catalog"` + IconUrl string `help:"icon url of service catalog"` + } + + R(&ServiceCatalogUpdateOptions{}, "service-catalog-update", "update a service catalog", + func(s *mcclient.ClientSession, opts *ServiceCatalogUpdateOptions) error { + params := jsonutils.NewDict() + if len(opts.Name) > 0 { + params.Add(jsonutils.NewString(opts.Name), "name") + } + if len(opts.GuestTemplate) > 0 { + params.Add(jsonutils.NewString(opts.GuestTemplate), "guest_template") + } + if len(opts.IconUrl) > 0 { + params.Add(jsonutils.NewString(opts.IconUrl), "icon_url") + } + sc, err := modules.ServiceCatalog.Update(s, opts.ID, params) + if err != nil { + return err + } + printObject(sc) + return nil + }, + ) + + type ServiceCatalogDeployOptions struct { + ServiceCatalogOptions + Name string `help:"Name of guest"` + GenerateName bool `help:"whether to generate name for guest"` + Count int `help:"count of guest"` + ProjectID string `help:"project id of guest"` + } + + R(&ServiceCatalogDeployOptions{}, "service-catalog-deploy", "deploy", func(s *mcclient.ClientSession, + opts *ServiceCatalogDeployOptions) error { + + params := jsonutils.NewDict() + if opts.GenerateName { + params.Add(jsonutils.NewString(opts.Name), "generate_name") + } else { + params.Add(jsonutils.NewString(opts.Name), "name") + } + if opts.Count != 0 { + params.Add(jsonutils.NewInt(int64(opts.Count)), "count") + } + if len(opts.ProjectID) > 0 { + params.Add(jsonutils.NewString(opts.ProjectID), "project_id") + } + + sc, err := modules.ServiceCatalog.PerformAction(s, opts.ID, "deploy", params) + if err != nil { + return err + } + printObject(sc) + return nil + }) + +} diff --git a/pkg/apis/compute/guesttemplate.go b/pkg/apis/compute/guesttemplate.go new file mode 100644 index 0000000000..936e7518c2 --- /dev/null +++ b/pkg/apis/compute/guesttemplate.go @@ -0,0 +1,75 @@ +package compute + +import ( + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis" +) + +type GuesttemplateCreateInput struct { + apis.Meta + + // description: guest template name + // unique: true + // required: true + // example: hello + Name string `json:"name"` + + // description: the content of guest template + // required: true + Content jsonutils.JSONObject `json:"content"` +} + +type GuesttemplateDetails struct { + apis.Meta + apis.SharableVirtualResourceDetails + SGuestTemplate + + Config GuesttemplateConfigInfo `json:"config"` +} + +type GuesttemplateConfigInfo struct { + Region string `json:"region"` + Zone string `json:"zone"` + Hypervisor string `json:"hypervisor"` + OsType string `json:"os_type"` + Sku GuesttemplateSku `json:"sku"` + Disks []GuesttemplateDisk `json:"disks"` + Keypair string `json:"keypair"` + Nets []GuesttemplateNetwork `json:"nets"` + Secgroup string `json:"secgroup"` + IsolatedDeviceConfig []IsolatedDeviceConfig `json:"isolated_device_config"` + Image string `json:"image"` +} + +type GuesttemplateDisk struct { + Backend string `json:"backend"` + DiskType string `json:"disk_type"` + Index int `json:"index"` + SizeMb int `json:"size_mb"` +} + +type GuesttemplateNetwork struct { + ID string `json:"id"` + Name string `json:"name"` + GuestIpStart string `json:"guest_ip_start"` + GuestIpEnd string `json:"guest_up_end"` + VlanId int `json:"vlan_id"` +} + +type GuesttemplateSku struct { + Name string `json:"name"` + CpuCoreCount int `json:"cpu_core_count"` + MemorySizeMb int `json:"memory_size_mb"` + InstanceTypeCategory string `json:"instance_type_category` + InstanceTypeFamily string `json:"instance_type_family"` +} + +type GuesttemplatePublicInput struct { + apis.Meta + + // description: the scope about public operator + // required: true + // example: system + Scope string `json:"scope"` +} diff --git a/pkg/apis/compute/service_compute.go b/pkg/apis/compute/service_compute.go new file mode 100644 index 0000000000..c73dc9bb16 --- /dev/null +++ b/pkg/apis/compute/service_compute.go @@ -0,0 +1,36 @@ +package compute + +import "yunion.io/x/onecloud/pkg/apis" + +type ServiceCatalogCreateInput struct { + apis.Meta + + // description: service catalog name + // uniqure: true + // required: true + // example: hello + Name string `json:"name` + + // description: service catalog icon url + // example: https://yunion.io/files/hello.png + IconUrl string `json:"icon_url"` + + // description: the id or name of guest template + // example: good + GuestTemplate string `json:"guest_template"` +} + +type ServiceCatalogDeploy struct { + + // description: name of the new vm + // example: hello + Name string `json:"name"` + + // description: generate name automatically if name is repeated, and only one of name and this shoudle be given + // example: hello + GenerateName string `json:"generate_name"` + + // description: the count of the new vm + // example: 1 + Count int `json:"count"` +} diff --git a/pkg/apis/compute/zz_generated.model.go b/pkg/apis/compute/zz_generated.model.go index 46c6171fa1..ca9bfd81ed 100644 --- a/pkg/apis/compute/zz_generated.model.go +++ b/pkg/apis/compute/zz_generated.model.go @@ -549,6 +549,18 @@ type SGuestJointsBase struct { GuestId string `json:"guest_id"` } +// SGuestTemplate is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestTemplate. +type SGuestTemplate struct { + apis.SSharableVirtualResourceBase + VcpuCount int `json:"vcpu_count"` + VmemSize int `json:"vmem_size"` + OsType string `json:"os_type"` + ImageType string `json:"image_type"` + ImageId string `json:"image_id"` + Hypervisor string `json:"hypervisor"` + Content interface{} `json:"content"` +} + // SGuestdisk is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SGuestdisk. type SGuestdisk struct { SGuestJointsBase @@ -1191,6 +1203,13 @@ type SServerSku struct { Provider string `json:"provider"` } +// SServiceCatalog is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SServiceCatalog. +type SServiceCatalog struct { + apis.SSharableVirtualResourceBase + IconUrl string `json:"icon_url"` + GuestTemplateID string `json:"guest_template_id"` +} + // SSnapshot is an autogenerated struct via yunion.io/x/onecloud/pkg/compute/models.SSnapshot. type SSnapshot struct { apis.SVirtualResourceBase diff --git a/pkg/cloudprovider/images.go b/pkg/cloudprovider/images.go index 8778e35a0d..c50600c62b 100644 --- a/pkg/cloudprovider/images.go +++ b/pkg/cloudprovider/images.go @@ -48,6 +48,7 @@ type SImage struct { SizeBytes int64 `json:"size"` Status string // UpdatedAt time.Time + PublicScope string } func CloudImage2Image(image ICloudImage) SImage { diff --git a/pkg/compute/models/guest_template.go b/pkg/compute/models/guest_template.go new file mode 100644 index 0000000000..ae7bd559bb --- /dev/null +++ b/pkg/compute/models/guest_template.go @@ -0,0 +1,397 @@ +// 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" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/sets" + + computeapis "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/cmdline" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/compute/options" + "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/logclient" + "yunion.io/x/onecloud/pkg/util/rbacutils" +) + +const ( + IMAGE_TYPE_NORMAL = "normal" + IMAGE_TYPE_GUEST = "guest" +) + +type SGuestTemplateManager struct { + db.SSharableVirtualResourceBaseManager +} + +type SGuestTemplate struct { + db.SSharableVirtualResourceBase + + VcpuCount int `nullable:"false" default:"1" create:"optional"` + VmemSize int `nullable:"false" create:"optional"` + OsType string `width:"36" charset:"ascii" nullable:"true" create:"optional"` + ImageType string `width:"10" charset:"ascii" nullabel:"true" default:"normal" create:"optional"` + ImageId string `width:"128" charset:"ascii" create:"optional"` + Hypervisor string `width:"16" charset:"ascii" default:"kvm" create:"optional"` + + Content jsonutils.JSONObject `nullable:"false" list:"user" update:"user" create:"optional"` +} + +var GuestTemplateManager *SGuestTemplateManager + +func init() { + GuestTemplateManager = &SGuestTemplateManager{ + SSharableVirtualResourceBaseManager: db.NewSharableVirtualResourceBaseManager( + SGuestTemplate{}, + "guesttemplates_tbl", + "servertemplate", + "servertemplates", + ), + } + + GuestTemplateManager.SetVirtualObject(GuestTemplateManager) +} + +func (gtm *SGuestTemplateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *computeapis.GuesttemplateCreateInput) (*jsonutils.JSONDict, error) { + + if input.Content == nil { + return nil, httperrors.NewMissingParameterError("content") + } + + data, err := gtm.validateData(ctx, userCred, ownerId, query, input) + if err != nil { + return nil, err + } + return gtm.SSharableVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, data) +} + +func (gt *SGuestTemplate) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + + logclient.AddActionLogWithContext(ctx, gt, logclient.ACT_CREATE, nil, userCred, true) +} + +func (gt *SGuestTemplate) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject) { + logclient.AddActionLogWithContext(ctx, gt, logclient.ACT_UPDATE, nil, userCred, true) +} + +func (gtm *SGuestTemplateManager) validateData(ctx context.Context, userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, cinput *computeapis.GuesttemplateCreateInput) (*jsonutils.JSONDict, error) { + if cinput.Content == nil { + return cinput.JSON(cinput), nil + } + content := cinput.Content + data := cinput.JSON(cinput) + // not support guest image and guest snapshot for now + if content.Contains("instance_snapshot_id") { + return nil, httperrors.NewInputParameterError( + "no support for instance snapshot in guest template for now") + } + // I don't hope cinput.Content same with data["content"] will change in GuestManager.validateCreateData + copy := jsonutils.DeepCopy(content).(*jsonutils.JSONDict) + input, err := GuestManager.validateCreateData(ctx, userCred, ownerId, query, copy) + if err != nil { + return nil, httperrors.NewInputParameterError(err.Error()) + } + // fill field + data.Add(jsonutils.NewInt(int64(input.VmemSize)), "vmem_size") + data.Add(jsonutils.NewInt(int64(input.VcpuCount)), "vcpu_count") + data.Add(jsonutils.NewString(input.OsType), "os_type") + data.Add(jsonutils.NewString(input.Hypervisor), "hypervisor") + + if len(input.GuestImageID) > 0 { + data.Add(jsonutils.NewString(IMAGE_TYPE_GUEST), "image_type") + data.Add(jsonutils.NewString(input.GuestImageID), "image_id") + } else { + data.Add(jsonutils.NewString(input.Disks[0].ImageId), "image_id") + data.Add(jsonutils.NewString(IMAGE_TYPE_NORMAL), "image_type") + } + + // hide some properties + contentDict := content.(*jsonutils.JSONDict) + contentDict.Remove("name") + contentDict.Remove("generate_name") + // "__count__" was converted to "count" by apigateway + contentDict.Remove("count") + contentDict.Remove("project_id") + + data.Add(contentDict, "content") + return data, nil +} + +func (gt *SGuestTemplate) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, cinput *computeapis.GuesttemplateCreateInput) (*jsonutils.JSONDict, error) { + + data, err := GuestTemplateManager.validateData(ctx, userCred, gt.GetOwnerId(), query, cinput) + if err != nil { + return nil, nil + } + return gt.SSharableVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, data) +} + +func (gt *SGuestTemplate) GetExtraDetails(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject) (*computeapis.GuesttemplateDetails, error) { + + out := &computeapis.GuesttemplateDetails{} + err := gt.SSharableVirtualResourceBase.GetExtraDetailsV2(ctx, userCred, query, &out.SharableVirtualResourceDetails) + if err != nil { + return out, err + } + gt.getMoreDetailsV2(ctx, userCred, out) + return out, nil +} + +func (gt *SGuestTemplate) getMoreDetailsV2(ctx context.Context, userCred mcclient.TokenCredential, + out *computeapis.GuesttemplateDetails) { + + input, err := cmdline.FetchServerCreateInputByJSON(gt.Content) + if err != nil { + return + } + configInfo := computeapis.GuesttemplateConfigInfo{} + if len(input.PreferRegion) != 0 { + region := CloudregionManager.FetchRegionById(input.PreferRegion) + if region != nil { + input.PreferRegion = region.GetName() + } + configInfo.Region = input.PreferRegion + + } + if len(input.PreferZone) != 0 { + zone := ZoneManager.FetchZoneById(input.PreferZone) + if zone != nil { + input.PreferZone = zone.GetName() + } + configInfo.Zone = input.PreferZone + } + configInfo.Hypervisor = gt.Hypervisor + configInfo.OsType = gt.OsType + + // sku deal + if len(input.InstanceType) > 0 { + skuOutput := computeapis.GuesttemplateSku{} + provider := GetDriver(gt.Hypervisor).GetProvider() + sku, err := ServerSkuManager.FetchSkuByNameAndProvider(input.InstanceType, provider, true) + if err != nil { + skuOutput.Name = input.InstanceType + skuOutput.MemorySizeMb = gt.VmemSize + skuOutput.CpuCoreCount = gt.VcpuCount + } else { + skuOutput.Name = sku.Name + skuOutput.MemorySizeMb = sku.MemorySizeMB + skuOutput.CpuCoreCount = sku.CpuCoreCount + skuOutput.InstanceTypeCategory = sku.InstanceTypeCategory + skuOutput.InstanceTypeFamily = sku.InstanceTypeFamily + } + configInfo.Sku = skuOutput + } + + // disk deal + disks := make([]computeapis.GuesttemplateDisk, len(input.Disks)) + for i := range input.Disks { + disks[i] = computeapis.GuesttemplateDisk{ + Backend: input.Disks[i].Backend, + DiskType: input.Disks[i].DiskType, + Index: input.Disks[i].Index, + SizeMb: input.Disks[i].SizeMb, + } + } + configInfo.Disks = disks + + // keypair + if len(input.KeypairId) > 0 { + model, err := KeypairManager.FetchById(input.KeypairId) + if err == nil { + keypair := model.(*SKeypair) + configInfo.Keypair = keypair.GetName() + } + } + + // network + if len(input.Networks) > 0 { + networkList := make([]computeapis.GuesttemplateNetwork, 0, len(input.Networks)) + networkIdList := make([]string, len(input.Networks)) + for i := range input.Networks { + networkIdList[i] = input.Networks[i].Network + } + networkSet := sets.NewString(networkIdList...) + + q := NetworkManager.Query("id", "name", "guest_ip_start", "guest_ip_end", "vlan_id").In("id", networkIdList) + q.All(&networkList) + + for _, p := range networkList { + if networkSet.Has(p.ID) { + networkSet.Delete(p.ID) + } + } + + // some specified network + for _, id := range networkSet.UnsortedList() { + networkList = append(networkList, computeapis.GuesttemplateNetwork{ID: id}) + } + + configInfo.Nets = networkList + } + + // secgroup + if len(input.SecgroupId) > 0 { + secgroup := SecurityGroupManager.FetchSecgroupById(input.SecgroupId) + if secgroup != nil { + input.SecgroupId = secgroup.GetName() + } + configInfo.Secgroup = input.SecgroupId + } + + // isolatedDevices + if input.IsolatedDevices != nil && len(input.IsolatedDevices) != 0 { + configInfo.IsolatedDeviceConfig = make([]computeapis.IsolatedDeviceConfig, len(input.IsolatedDevices)) + for i := range configInfo.IsolatedDeviceConfig { + configInfo.IsolatedDeviceConfig[i] = *input.IsolatedDevices[i] + } + } + + // fill image info + switch gt.ImageType { + case IMAGE_TYPE_NORMAL: + image, err := CachedimageManager.getImageInfo(ctx, userCred, gt.ImageId, false) + if err == nil { + configInfo.Image = image.Name + } else { + configInfo.Image = gt.ImageId + } + case IMAGE_TYPE_GUEST: + s := auth.GetSession(ctx, userCred, options.Options.Region, "") + ret, err := modules.GuestImages.Get(s, gt.ImageId, jsonutils.JSONNull) + if err != nil || !ret.Contains("id") { + configInfo.Image = gt.ImageId + } else { + name, _ := ret.GetString("id") + configInfo.Image = name + } + default: + // no arrivals + } + + out.Config = configInfo + return +} + +func (gt *SGuestTemplate) PerformPublic(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + + // image, network, secgroup, instancegroup + input, err := cmdline.FetchServerCreateInputByJSON(gt.Content) + if err != nil { + return nil, errors.Wrap(err, "fail to convert content of guest template to ServerCreateInput") + } + + // check for below private resource in the guest template + privateResource := map[string]int{ + "keypair": len(input.KeypairId), + "instance group": len(input.InstanceGroupIds), + "instance snapshot": len(input.InstanceSnapshotId), + } + for k, v := range privateResource { + if v > 0 { + return nil, gt.genForbiddenError(k, "", "") + } + } + + targetScopeStr, _ := data.GetString("scope") + targetScope := rbacutils.String2ScopeDefault(targetScopeStr, rbacutils.ScopeSystem) + + // check if secgroup is public + if len(input.SecgroupId) > 0 { + model, err := SecurityGroupManager.FetchByIdOrName(userCred, input.SecgroupId) + if err != nil { + return nil, httperrors.NewResourceNotFoundError("there is no such secgroup %s descripted by guest template", + input.SecgroupId) + } + secgroup := model.(*SSecurityGroup) + sgScope := rbacutils.String2Scope(secgroup.PublicScope) + if !secgroup.IsPublic || !sgScope.HigherEqual(targetScope) { + return nil, gt.genForbiddenError("security group", input.SecgroupId, string(targetScope)) + } + } + + // check if networks is public + if len(input.Networks) > 0 { + for i := range input.Networks { + str := input.Networks[i].Network + model, err := NetworkManager.FetchByIdOrName(userCred, str) + if err != nil { + return nil, httperrors.NewResourceNotFoundError( + "there is no such secgroup %s descripted by guest template") + } + network := model.(*SNetwork) + netScope := rbacutils.String2Scope(network.PublicScope) + if !network.IsPublic || !netScope.HigherEqual(targetScope) { + return nil, gt.genForbiddenError("network", str, string(targetScope)) + } + } + } + + // check if image is public + var ( + isPublic bool + publicScope string + ) + switch gt.ImageType { + case IMAGE_TYPE_NORMAL: + image, err := CachedimageManager.GetImageById(ctx, userCred, gt.ImageId, false) + if err != nil { + return nil, errors.Wrapf(err, "fail to fetch image %s descripted by guest template", gt.ImageId) + } + isPublic, publicScope = image.IsPublic, image.PublicScope + case IMAGE_TYPE_GUEST: + s := auth.GetSession(ctx, userCred, options.Options.Region, "") + ret, err := modules.GuestImages.Get(s, gt.ImageId, jsonutils.JSONNull) + if err != nil { + return nil, errors.Wrapf(err, "fail to fetch guest image %s descripted by guest template", gt.ImageId) + } + isPublic = jsonutils.QueryBoolean(ret, "is_public", false) + publicScope, _ = ret.GetString("public_scope") + default: + //no arrivals + } + igScope := rbacutils.String2Scope(publicScope) + if !isPublic || !igScope.HigherEqual(targetScope) { + return nil, gt.genForbiddenError("image", "", string(targetScope)) + } + + return gt.SSharableVirtualResourceBase.PerformPublic(ctx, userCred, query, data) +} + +func (gt *SGuestTemplate) genForbiddenError(resourceName, resourceStr, scope string) error { + var msg string + if len(resourceStr) == 0 { + msg = fmt.Sprintf("the %s in guest template is not a public resource", resourceName) + } else { + msg = fmt.Sprintf("the %s '%s' in guest template is not a public resource", resourceName, resourceStr) + } + if len(scope) > 0 { + msg += fmt.Sprintf(" in %s scope", scope) + } + return httperrors.NewForbiddenError(msg) +} diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 462a386731..7a786cc73d 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -4877,6 +4877,12 @@ func (self *SGuestManager) checkGuestImage(ctx context.Context, input *api.Serve return errors.Wrap(err, "get guest image from glance error") } + // input.GuestImageID maybe name of guestimage + if ret.Contains("id") { + id, _ := ret.GetString("id") + input.GuestImageID = id + } + images := &api.SImagesInGuest{} err = ret.Unmarshal(images) if err != nil { diff --git a/pkg/compute/models/service_catalog.go b/pkg/compute/models/service_catalog.go new file mode 100644 index 0000000000..51979737a7 --- /dev/null +++ b/pkg/compute/models/service_catalog.go @@ -0,0 +1,136 @@ +// 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" + "database/sql" + "net/url" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + computeapis "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/compute/options" + "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" +) + +type SServiceCatalogManager struct { + db.SSharableVirtualResourceBaseManager +} + +type SServiceCatalog struct { + db.SSharableVirtualResourceBase + + IconUrl string `charset:"ascii" create:"optional" list:"user" get:"user"` + GuestTemplateID string `width:"128" charset:"ascii" create:"optional" list:"user" get:"user"` +} + +var ServiceCatalogManager *SServiceCatalogManager + +func init() { + ServiceCatalogManager = &SServiceCatalogManager{ + SSharableVirtualResourceBaseManager: db.NewSharableVirtualResourceBaseManager( + SServiceCatalog{}, + "servicecatalogs_tbl", + "servicecatalog", + "servicecatalogs", + ), + } + ServiceCatalogManager.SetVirtualObject(ServiceCatalogManager) +} + +func (scm *SServiceCatalogManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *computeapis.ServiceCatalogCreateInput) (*jsonutils.JSONDict, + error) { + + if len(input.GuestTemplate) == 0 { + return nil, httperrors.NewMissingParameterError("guest_template") + } + + model, err := GuestTemplateManager.FetchByIdOrName(userCred, input.GuestTemplate) + if errors.Cause(err) == sql.ErrNoRows { + return nil, httperrors.NewResourceNotFoundError("no such guest template") + } + if err != nil { + return nil, err + } + gt := model.(*SGuestTemplate) + //scope := rbacutils.String2Scope(gt.PublicScope) + //if !gt.IsPublic || scope != rbacutils.ScopeSystem { + // return nil, httperrors.NewForbiddenError("guest template must be public in scope system") + //} + if userCred.GetProjectId() != gt.ProjectId { + return nil, httperrors.NewForbiddenError("guest template must has same project id with the request") + } + + data := input.JSON(input) + data.Remove("guest_template") + data.Add(jsonutils.NewString(model.GetId()), "guest_template_id") + + // check url + if len(input.IconUrl) == 0 { + return data, nil + } + url, err := url.Parse(input.IconUrl) + if err != nil { + return nil, httperrors.NewInputParameterError("fail to parse icon url '%s'", input.IconUrl) + } + data.Add(jsonutils.NewString(url.String()), "icon_url") + return data, nil +} + +func (sc *SServiceCatalog) AllowPerformDeploy(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + + return sc.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, sc, "deploy") +} + +func (sc *SServiceCatalog) PerformDeploy(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, input *computeapis.ServiceCatalogDeploy) (jsonutils.JSONObject, error) { + + if len(input.Name) == 0 && len(input.GenerateName) == 0 { + return nil, httperrors.NewMissingParameterError("name or generate_name") + } + model, err := GuestTemplateManager.FetchById(sc.GuestTemplateID) + if errors.Cause(err) == sql.ErrNoRows { + return nil, httperrors.NewResourceNotFoundError("no such guest_template %s", sc.GuestTemplateID) + } + if err != nil { + return nil, err + } + guestTempalte := model.(*SGuestTemplate) + content := guestTempalte.Content + contentDict := content.(*jsonutils.JSONDict) + if len(input.GenerateName) != 0 { + contentDict.Add(jsonutils.NewString(input.GenerateName), "generate_name") + } else { + contentDict.Add(jsonutils.NewString(input.Name), "name") + } + if input.Count != 0 { + input.Count = 1 + } + contentDict.Add(jsonutils.NewInt(int64(input.Count)), "count") + s := auth.GetSession(ctx, userCred, options.Options.Region, "") + _, err = modules.Servers.Create(s, content) + if err != nil { + return nil, errors.Wrap(err, "fail to create guest") + } + return nil, err +} diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index 4ac9629103..fb8e3be230 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -137,6 +137,9 @@ func InitHandlers(app *appsrv.Application) { models.ElasticcacheBackupManager, models.ElasticcacheSkuManager, models.GlobalNetworkManager, + + models.GuestTemplateManager, + models.ServiceCatalogManager, } { db.RegisterModelManager(manager) handler := db.NewModelHandler(manager) diff --git a/pkg/mcclient/modules/mod_guesttemplate.go b/pkg/mcclient/modules/mod_guesttemplate.go new file mode 100644 index 0000000000..452411913a --- /dev/null +++ b/pkg/mcclient/modules/mod_guesttemplate.go @@ -0,0 +1,30 @@ +// 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 SGuestTemplateManager struct { + modulebase.ResourceManager +} + +var GuestTemplate SGuestTemplateManager + +func init() { + GuestTemplate = SGuestTemplateManager{NewComputeManager("servertemplate", "servertemplates", + []string{"ID", "Name", "Public_Scope", "Is_Public", "Project_Id", "Content"}, + []string{})} + registerCompute(&GuestTemplate) +} diff --git a/pkg/mcclient/modules/mod_servicecatalog.go b/pkg/mcclient/modules/mod_servicecatalog.go new file mode 100644 index 0000000000..a25c6babfd --- /dev/null +++ b/pkg/mcclient/modules/mod_servicecatalog.go @@ -0,0 +1,31 @@ +// 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 SServiceCatalog struct { + modulebase.ResourceManager +} + +var ServiceCatalog SServiceCatalog + +func init() { + ServiceCatalog = SServiceCatalog{NewComputeManager("servicecatalog", "servicecatalogs", + []string{"ID", "Name", "Public_Scope", "Is_Public", "Icon_Url", "Guest_Template_ID"}, + []string{})} + + registerCompute(&ServiceCatalog) +} diff --git a/pkg/mcclient/options/servers.go b/pkg/mcclient/options/servers.go index 3553a8552d..1b76c89752 100644 --- a/pkg/mcclient/options/servers.go +++ b/pkg/mcclient/options/servers.go @@ -245,9 +245,14 @@ type ServerCreateFromInstanceSnapshot struct { } type ServerCreateOptions struct { + ServerCreateOptionalOptions + + NAME string `help:"Name of server" json:"-"` +} + +type ServerCreateOptionalOptions struct { ServerConfigs - NAME string `help:"Name of server" json:"-"` MemSpec string `help:"Memory size Or Instance Type" metavar:"MEMSPEC" json:"-"` Keypair string `help:"SSH Keypair"` @@ -340,7 +345,7 @@ func (o *ServerCreateOptions) ToScheduleInput() (*schedapi.ScheduleInput, error) return input, nil } -func (opts *ServerCreateOptions) Params() (*computeapi.ServerCreateInput, error) { +func (opts *ServerCreateOptionalOptions) OptionalParams() (*computeapi.ServerCreateInput, error) { config, err := opts.ServerConfigs.Data() if err != nil { return nil, err @@ -370,11 +375,6 @@ func (opts *ServerCreateOptions) Params() (*computeapi.ServerCreateInput, error) Secgroups: opts.Secgroups, } - if opts.GenerateName { - params.GenerateName = opts.NAME - } else { - params.Name = opts.NAME - } if regutils.MatchSize(opts.MemSpec) { memSize, err := fileutils.GetSizeMb(opts.MemSpec, 'M', 1024) if err != nil { @@ -429,6 +429,22 @@ func (opts *ServerCreateOptions) Params() (*computeapi.ServerCreateInput, error) return params, nil } +func (opts *ServerCreateOptions) Params() (*computeapi.ServerCreateInput, error) { + + params, err := opts.OptionalParams() + if err != nil { + return nil, err + } + + if opts.GenerateName { + params.GenerateName = opts.NAME + } else { + params.Name = opts.NAME + } + + return params, nil +} + type ServerStopOptions struct { ID []string `help:"ID or Name of server" json:"-"` Force *bool `help:"Stop server forcefully" json:"is_force"`