diff --git a/cmd/climc/climc.go b/cmd/climc/climc.go index ee6ae56f6c..d68b579b8f 100644 --- a/cmd/climc/climc.go +++ b/cmd/climc/climc.go @@ -49,14 +49,19 @@ type BaseOptions struct { Completion string `default:"" help:"Generate climc auto complete script" choices:"bash"` UseCachedToken bool `default:"$YUNION_USE_CACHED_TOKEN|false" help:"Use cached token"` - OsUsername string `default:"$OS_USERNAME" help:"Username, defaults to env[OS_USERNAME]"` - OsPassword string `default:"$OS_PASSWORD" help:"Password, defaults to env[OS_PASSWORD]"` + + OsUsername string `default:"$OS_USERNAME" help:"Username, defaults to env[OS_USERNAME]"` + OsPassword string `default:"$OS_PASSWORD" help:"Password, defaults to env[OS_PASSWORD]"` // OsProjectId string `default:"$OS_PROJECT_ID" help:"Proejct ID, defaults to env[OS_PROJECT_ID]"` OsProjectName string `default:"$OS_PROJECT_NAME" help:"Project name, defaults to env[OS_PROJECT_NAME]"` OsProjectDomain string `default:"$OS_PROJECT_DOMAIN" help:"Domain name of project, defaults to env[OS_PROJECT_DOMAIN]"` + OsDomainName string `default:"$OS_DOMAIN_NAME" help:"Domain name, defaults to env[OS_DOMAIN_NAME]"` + + OsAccessKey string `default:"$OS_ACCESS_KEY" help:"ak/sk access key, defaults to env[OS_ACCESS_KEY]"` + OsSecretKey string `default:"$OS_SECRET_KEY" help:"ak/s secret, defaults to env[OS_SECRET_KEY]"` + + OsAuthURL string `default:"$OS_AUTH_URL" help:"Defaults to env[OS_AUTH_URL]"` - OsDomainName string `default:"$OS_DOMAIN_NAME" help:"Domain name, defaults to env[OS_DOMAIN_NAME]"` - OsAuthURL string `default:"$OS_AUTH_URL" help:"Defaults to env[OS_AUTH_URL]"` OsRegionName string `default:"$OS_REGION_NAME" help:"Defaults to env[OS_REGION_NAME]"` OsZoneName string `default:"$OS_ZONE_NAME" help:"Defaults to env[OS_ZONE_NAME]"` OsEndpointType string `default:"$OS_ENDPOINT_TYPE|internalURL" help:"Defaults to env[OS_ENDPOINT_TYPE] or internalURL" choices:"publicURL|internalURL|adminURL"` @@ -124,20 +129,18 @@ func newClientSession(options *BaseOptions) (*mcclient.ClientSession, error) { if len(options.OsAuthURL) == 0 { return nil, fmt.Errorf("Missing OS_AUTH_URL") } - if len(options.OsUsername) == 0 { - return nil, fmt.Errorf("Missing OS_USERNAME") + if len(options.OsUsername) == 0 && len(options.OsAccessKey) == 0 { + return nil, fmt.Errorf("Missing OS_USERNAME or OS_ACCESS_KEY") } - if len(options.OsPassword) == 0 { + if len(options.OsUsername) > 0 && len(options.OsPassword) == 0 { return nil, fmt.Errorf("Missing OS_PASSWORD") } + if len(options.OsAccessKey) > 0 && len(options.OsSecretKey) == 0 { + return nil, fmt.Errorf("Missing OS_SECRET_KEY") + } if len(options.OsRegionName) == 0 { return nil, fmt.Errorf("Missing OS_REGION_NAME") } - // if len(options.OsProjectId) == 0 && len(options.OsProjectName) == 0 { - // showErrorAndExit(fmt.Errorf("Missing OS_PROEJCT_ID or OS_PROJECT_NAME")) - // if len(options.OsProjectName) == 0 { - // return nil, fmt.Errorf("Missing OS_PROJECT_NAME") - // } logLevel := "info" if options.Debug { @@ -178,12 +181,19 @@ func newClientSession(options *BaseOptions) (*mcclient.ClientSession, error) { } if cacheToken == nil { - token, err := client.AuthenticateWithSource(options.OsUsername, - options.OsPassword, - options.OsDomainName, - options.OsProjectName, - options.OsProjectDomain, - mcclient.AuthSourceCli) + var token mcclient.TokenCredential + var err error + if len(options.OsAccessKey) > 0 { + token, err = client.AuthenticateByAccessKey(options.OsAccessKey, + options.OsSecretKey, mcclient.AuthSourceCli) + } else { + token, err = client.AuthenticateWithSource(options.OsUsername, + options.OsPassword, + options.OsDomainName, + options.OsProjectName, + options.OsProjectDomain, + mcclient.AuthSourceCli) + } if err != nil { return nil, err } diff --git a/cmd/climc/shell/cloudaccounts.go b/cmd/climc/shell/cloudaccounts.go index f7d6428e9c..78ebb2e999 100644 --- a/cmd/climc/shell/cloudaccounts.go +++ b/cmd/climc/shell/cloudaccounts.go @@ -189,6 +189,17 @@ func init() { return nil }) + R(&options.SS3CloudAccountCreateOptions{}, "cloud-account-create-ceph", "Create a ceph object storage account", func(s *mcclient.ClientSession, args *options.SS3CloudAccountCreateOptions) error { + params := jsonutils.Marshal(args) + params.(*jsonutils.JSONDict).Add(jsonutils.NewString("Ceph"), "provider") + result, err := modules.Cloudaccounts.Create(s, params) + if err != nil { + return err + } + printObject(result) + return nil + }) + type CloudaccountUpdateOptions struct { ID string `help:"ID or Name of cloud account"` Name string `help:"New name to update"` diff --git a/cmd/climc/shell/credentials.go b/cmd/climc/shell/credentials.go index 0006eda478..029d182630 100644 --- a/cmd/climc/shell/credentials.go +++ b/cmd/climc/shell/credentials.go @@ -185,9 +185,13 @@ func init() { }) R(&CredentialAkSkOptions{}, "credential-get-aksk", "Get AccessKey/Secret credential for user and project", func(s *mcclient.ClientSession, args *CredentialAkSkOptions) error { - uid, err := modules.UsersV3.FetchId(s, args.User, args.UserDomain) - if err != nil { - return err + var uid string + var err error + if len(args.User) > 0 { + uid, err = modules.UsersV3.FetchId(s, args.User, args.UserDomain) + if err != nil { + return err + } } var pid string if len(args.Project) > 0 { diff --git a/cmd/s3cli/main.go b/cmd/s3cli/main.go index abaf54d531..9381beb31c 100644 --- a/cmd/s3cli/main.go +++ b/cmd/s3cli/main.go @@ -20,7 +20,10 @@ import ( "yunion.io/x/structarg" + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud/objectstore" + "yunion.io/x/onecloud/pkg/multicloud/objectstore/ceph" _ "yunion.io/x/onecloud/pkg/multicloud/objectstore/shell" "yunion.io/x/onecloud/pkg/util/shellutils" ) @@ -31,6 +34,7 @@ type BaseOptions struct { AccessUrl string `help:"Access url" default:"$S3_ACCESS_URL"` AccessKey string `help:"Access key" default:"$S3_ACCESS_KEY"` Secret string `help:"Secret" default:"$S3_SECRET"` + Backend string `help:"Backend driver" default:"$S3_BACKEND"` SUBCOMMAND string `help:"s3cli subcommand" subcommand:"true"` } @@ -75,7 +79,7 @@ func showErrorAndExit(e error) { os.Exit(1) } -func newClient(options *BaseOptions) (*objectstore.SObjectStoreClient, error) { +func newClient(options *BaseOptions) (cloudprovider.ICloudRegion, error) { if len(options.AccessUrl) == 0 { return nil, fmt.Errorf("Missing accessUrl") } @@ -88,6 +92,9 @@ func newClient(options *BaseOptions) (*objectstore.SObjectStoreClient, error) { return nil, fmt.Errorf("Missing secret") } + if options.Backend == api.CLOUD_PROVIDER_CEPH { + return ceph.NewCephRados("", "", options.AccessUrl, options.AccessKey, options.Secret, options.Debug) + } return objectstore.NewObjectStoreClient("", "", options.AccessUrl, options.AccessKey, options.Secret, options.Debug) } @@ -116,7 +123,7 @@ func main() { if options.SUBCOMMAND == "help" { e = subcmd.Invoke(suboptions) } else { - var client *objectstore.SObjectStoreClient + var client cloudprovider.ICloudRegion client, e = newClient(options) if e != nil { showErrorAndExit(e) diff --git a/pkg/apis/compute/cloudaccount_const.go b/pkg/apis/compute/cloudaccount_const.go index 6c00de2c0a..eaae00ef95 100644 --- a/pkg/apis/compute/cloudaccount_const.go +++ b/pkg/apis/compute/cloudaccount_const.go @@ -41,6 +41,7 @@ const ( CLOUD_PROVIDER_ZSTACK = "ZStack" CLOUD_PROVIDER_GENERICS3 = "S3" + CLOUD_PROVIDER_CEPH = "Ceph" CLOUD_PROVIDER_HEALTH_NORMAL = "normal" // 远端处于健康状态 CLOUD_PROVIDER_HEALTH_INSUFFICIENT = "insufficient" // 不足按需资源余额 diff --git a/pkg/apis/compute/nat.go b/pkg/apis/compute/nat.go index 13f2325353..e14e64d39d 100644 --- a/pkg/apis/compute/nat.go +++ b/pkg/apis/compute/nat.go @@ -1,3 +1,17 @@ +// 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" diff --git a/pkg/cloudprovider/objectstore.go b/pkg/cloudprovider/objectstore.go index 83891defe8..1c834b622a 100644 --- a/pkg/cloudprovider/objectstore.go +++ b/pkg/cloudprovider/objectstore.go @@ -16,13 +16,13 @@ package cloudprovider import ( "context" + "fmt" "io" "regexp" "strconv" "strings" "time" - "fmt" "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/s3cli" diff --git a/pkg/compute/models/buckets.go b/pkg/compute/models/buckets.go index a07dc50292..67634e6800 100644 --- a/pkg/compute/models/buckets.go +++ b/pkg/compute/models/buckets.go @@ -37,8 +37,10 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/cloudcommon/validators" "yunion.io/x/onecloud/pkg/cloudprovider" + "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" @@ -488,10 +490,36 @@ func (bucket *SBucket) GetExtraDetails(ctx context.Context, userCred mcclient.To return bucket.getMoreDetails(extra), nil } +func joinPath(ep, path string) string { + return strings.TrimRight(ep, "/") + "/" + strings.TrimLeft(path, "/") +} + func (bucket *SBucket) getMoreDetails(extra *jsonutils.JSONDict) *jsonutils.JSONDict { info := bucket.getCloudProviderInfo() extra.Update(jsonutils.Marshal(&info)) + s3gwUrl, _ := auth.GetServiceURL("s3gateway", options.Options.Region, "", "public") + if len(s3gwUrl) > 0 { + accessUrls := make([]cloudprovider.SBucketAccessUrl, 0) + err := bucket.AccessUrls.Unmarshal(&accessUrls) + if err == nil { + find := false + for i := range accessUrls { + if strings.HasPrefix(accessUrls[i].Url, s3gwUrl) { + find = true + break + } + } + if !find { + accessUrls = append(accessUrls, cloudprovider.SBucketAccessUrl{ + Url: joinPath(s3gwUrl, bucket.Name), + Description: "s3gateway", + }) + extra.Set("access_urls", jsonutils.Marshal(accessUrls)) + } + } + } + return extra } diff --git a/pkg/compute/service/service.go b/pkg/compute/service/service.go index 39a115d51b..a8a3e67e5a 100644 --- a/pkg/compute/service/service.go +++ b/pkg/compute/service/service.go @@ -34,16 +34,8 @@ import ( _ "yunion.io/x/onecloud/pkg/compute/regiondrivers" _ "yunion.io/x/onecloud/pkg/compute/storagedrivers" _ "yunion.io/x/onecloud/pkg/compute/tasks" - _ "yunion.io/x/onecloud/pkg/multicloud/aliyun/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/aws/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/azure/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/esxi/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/huawei/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/objectstore/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/openstack/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/qcloud/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/ucloud/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/zstack/provider" + + _ "yunion.io/x/onecloud/pkg/multicloud/loader" ) func StartService() { diff --git a/pkg/mcclient/aksk.go b/pkg/mcclient/aksk.go index a222750440..54c42efbc9 100644 --- a/pkg/mcclient/aksk.go +++ b/pkg/mcclient/aksk.go @@ -21,6 +21,8 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + "io" + "strings" api "yunion.io/x/onecloud/pkg/apis/identity" "yunion.io/x/onecloud/pkg/util/netutils2" "yunion.io/x/onecloud/pkg/util/s3auth" @@ -70,3 +72,38 @@ func (this *Client) VerifyRequest(req http.Request, aksk s3auth.IAccessKeySecret return token, nil } + +func jsonReader(input interface{}) io.Reader { + return strings.NewReader(jsonutils.Marshal(input).String()) +} + +func (this *Client) AuthenticateByAccessKey(accessKey string, secret string, source string) (TokenCredential, error) { + aCtx := SAuthContext{Source: source} + + seedAksk := s3auth.NewV4Request() + seedAksk.AccessKey = accessKey + seedAksk.Location = "cn-beijing" + input := SAuthenticationInputV3{} + input.Auth.Identity.Methods = []string{api.AUTH_METHOD_AKSK} + input.Auth.Identity.AccessKeyRequest = seedAksk.Encode() + input.Auth.Context = aCtx + + urlStr := joinUrl(this.authUrl, "/auth/tokens") + req, err := http.NewRequest(http.MethodPost, urlStr, jsonReader(input)) + if err != nil { + return nil, errors.Wrap(err, "http.NewRequest") + } + newreq := s3auth.SignV4(*req, seedAksk.AccessKey, secret, seedAksk.Location, jsonReader(input)) + + aksk, err := s3auth.DecodeAccessKeyRequest(*newreq, false) + if err != nil { + return nil, errors.Wrap(err, "s3auth.DecodeAccessKeyRequest") + } + + token, err := this._verifyKeySecret(aksk, aCtx) + if err != nil { + return nil, errors.Wrap(err, "this._verifyKeySecret") + } + + return token.Token, nil +} diff --git a/pkg/mcclient/options/cloudaccounts.go b/pkg/mcclient/options/cloudaccounts.go index 6a84efa718..b1648ac4a2 100644 --- a/pkg/mcclient/options/cloudaccounts.go +++ b/pkg/mcclient/options/cloudaccounts.go @@ -136,7 +136,7 @@ type SZStackCloudAccountCreateOptions struct { type SS3CloudAccountCreateOptions struct { SCloudAccountCreateBaseOptions SAccessKeyCredential - Endpoint string `help:"S3 endpoint" poisitional:"true" json:"endpoint"` + Endpoint string `help:"S3 endpoint" required:"true" positional:"true" json:"endpoint"` } // update credential options diff --git a/pkg/multicloud/loader/loader.go b/pkg/multicloud/loader/loader.go new file mode 100644 index 0000000000..ef3642522b --- /dev/null +++ b/pkg/multicloud/loader/loader.go @@ -0,0 +1,42 @@ +// 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 loader + +import ( + "yunion.io/x/log" + + // on-premise virtualization technologies + _ "yunion.io/x/onecloud/pkg/multicloud/esxi/provider" + + // private clouds + _ "yunion.io/x/onecloud/pkg/multicloud/openstack/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/zstack/provider" + + // public clouds + _ "yunion.io/x/onecloud/pkg/multicloud/aliyun/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/aws/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/azure/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/huawei/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/qcloud/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/ucloud/provider" + + // object storages + _ "yunion.io/x/onecloud/pkg/multicloud/objectstore/ceph/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/objectstore/provider" +) + +func init() { + log.Infof("Loading cloud providers ...") +} diff --git a/pkg/multicloud/objectstore/buckets.go b/pkg/multicloud/objectstore/buckets.go index 2f8016c1e2..a935baf1bb 100644 --- a/pkg/multicloud/objectstore/buckets.go +++ b/pkg/multicloud/objectstore/buckets.go @@ -18,14 +18,14 @@ import ( "context" "fmt" "io" - "path" + "net/http" + "strings" "time" "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" "yunion.io/x/s3cli" - "net/http" "yunion.io/x/onecloud/pkg/cloudprovider" "yunion.io/x/onecloud/pkg/multicloud" ) @@ -33,13 +33,16 @@ import ( type SBucket struct { multicloud.SBaseBucket - client *SObjectStoreClient + client IBucketProvider Name string Location string CreatedAt time.Time StorageClass string - Acl string +} + +func (bucket *SBucket) GetIBucketProvider() IBucketProvider { + return bucket.client } func (bucket *SBucket) GetId() string { @@ -104,10 +107,14 @@ func (bucket *SBucket) GetStats() cloudprovider.SBucketStats { return stats } +func joinPath(ep, path string) string { + return strings.TrimRight(ep, "/") + "/" + strings.TrimLeft(path, "/") +} + func (bucket *SBucket) GetAccessUrls() []cloudprovider.SBucketAccessUrl { return []cloudprovider.SBucketAccessUrl{ { - Url: path.Join(bucket.client.endpoint, bucket.Name), + Url: joinPath(bucket.client.GetEndpoint(), bucket.Name), Description: fmt.Sprintf("%s", bucket.Location), Primary: true, }, @@ -116,7 +123,7 @@ func (bucket *SBucket) GetAccessUrls() []cloudprovider.SBucketAccessUrl { func (bucket *SBucket) ListObjects(prefix string, marker string, delimiter string, maxCount int) (cloudprovider.SListObjectResult, error) { ret := cloudprovider.SListObjectResult{} - result, err := bucket.client.client.ListObjectsQuery(bucket.Name, prefix, marker, delimiter, maxCount) + result, err := bucket.client.S3Client().ListObjectsQuery(bucket.Name, prefix, marker, delimiter, maxCount) if err != nil { return ret, errors.Wrap(err, "ListObjectsQuery") } @@ -154,7 +161,7 @@ func (bucket *SBucket) GetIObjects(prefix string, isRecursive bool) ([]cloudprov defer close(doneCh) ret := make([]cloudprovider.ICloudObject, 0) - objectCh := bucket.client.client.ListObjects(bucket.Name, prefix, isRecursive, doneCh) + objectCh := bucket.client.S3Client().ListObjects(bucket.Name, prefix, isRecursive, doneCh) for object := range objectCh { if object.Err != nil { return nil, errors.Wrap(object.Err, "ListObjects") @@ -187,7 +194,7 @@ func (bucket *SBucket) PutObject(ctx context.Context, key string, input io.Reade opts.StorageClass = storageClassStr } opts.PartSize = uint64(cloudprovider.MAX_PUT_OBJECT_SIZEBYTES) - _, err := bucket.client.client.PutObjectDo(ctx, bucket.Name, key, input, "", "", sizeBytes, opts) + _, err := bucket.client.S3Client().PutObjectDo(ctx, bucket.Name, key, input, "", "", sizeBytes, opts) if err != nil { return errors.Wrap(err, "PutObjectWithContext") } @@ -210,7 +217,7 @@ func (bucket *SBucket) NewMultipartUpload(ctx context.Context, key string, contT if len(storageClassStr) > 0 { opts.StorageClass = storageClassStr } - result, err := bucket.client.client.InitiateMultipartUpload(ctx, bucket.Name, key, opts) + result, err := bucket.client.S3Client().InitiateMultipartUpload(ctx, bucket.Name, key, opts) if err != nil { return "", errors.Wrap(err, "InitiateMultipartUpload") } @@ -218,7 +225,7 @@ func (bucket *SBucket) NewMultipartUpload(ctx context.Context, key string, contT } func (bucket *SBucket) UploadPart(ctx context.Context, key string, uploadId string, partIndex int, input io.Reader, partSize int64) (string, error) { - part, err := bucket.client.client.UploadPart(ctx, bucket.Name, key, uploadId, input, partIndex, "", "", partSize, nil) + part, err := bucket.client.S3Client().UploadPart(ctx, bucket.Name, key, uploadId, input, partIndex, "", "", partSize, nil) if err != nil { return "", errors.Wrap(err, "UploadPart") } @@ -234,7 +241,7 @@ func (bucket *SBucket) CompleteMultipartUpload(ctx context.Context, key string, ETag: partEtags[i], } } - _, err := bucket.client.client.CompleteMultipartUpload(ctx, bucket.Name, key, uploadId, complete) + _, err := bucket.client.S3Client().CompleteMultipartUpload(ctx, bucket.Name, key, uploadId, complete) if err != nil { return errors.Wrap(err, "CompleteMultipartUpload") } @@ -242,7 +249,7 @@ func (bucket *SBucket) CompleteMultipartUpload(ctx context.Context, key string, } func (bucket *SBucket) AbortMultipartUpload(ctx context.Context, key string, uploadId string) error { - err := bucket.client.client.AbortMultipartUpload(ctx, bucket.Name, key, uploadId) + err := bucket.client.S3Client().AbortMultipartUpload(ctx, bucket.Name, key, uploadId) if err != nil { return errors.Wrap(err, "AbortMultipartUpload") } @@ -250,7 +257,7 @@ func (bucket *SBucket) AbortMultipartUpload(ctx context.Context, key string, upl } func (bucket *SBucket) DeleteObject(ctx context.Context, key string) error { - err := bucket.client.client.RemoveObject(bucket.Name, key) + err := bucket.client.S3Client().RemoveObject(bucket.Name, key) if err != nil { return errors.Wrap(err, "RemoveObject") } @@ -261,7 +268,7 @@ func (bucket *SBucket) GetTempUrl(method string, key string, expire time.Duratio if method != "GET" && method != "PUT" && method != "DELETE" { return "", errors.Error("unsupported method") } - url, err := bucket.client.client.Presign(method, bucket.Name, key, expire, nil) + url, err := bucket.client.S3Client().Presign(method, bucket.Name, key, expire, nil) if err != nil { return "", errors.Wrap(err, "Presign") } @@ -281,7 +288,7 @@ func (bucket *SBucket) CopyObject(ctx context.Context, destKey string, srcBucket return errors.Wrap(err, "NewDestinationInfo") } src := s3cli.NewSourceInfo(srcBucket, srcKey, nil) - err = bucket.client.client.CopyObject(dest, src) + err = bucket.client.S3Client().CopyObject(dest, src) if err != nil { return errors.Wrap(err, "CopyObject") } @@ -301,7 +308,7 @@ func (bucket *SBucket) GetObject(ctx context.Context, key string, rangeOpt *clou if rangeOpt != nil { opts.SetRange(rangeOpt.Start, rangeOpt.End) } - output, err := bucket.client.client.GetObject(bucket.Name, key, opts) + output, err := bucket.client.S3Client().GetObject(bucket.Name, key, opts) if err != nil { return nil, errors.Wrap(err, "GetObject") } @@ -309,7 +316,7 @@ func (bucket *SBucket) GetObject(ctx context.Context, key string, rangeOpt *clou } func (bucket *SBucket) CopyPart(ctx context.Context, key string, uploadId string, partNumber int, srcBucket string, srcKey string, srcOffset int64, srcLength int64) (string, error) { - result, err := bucket.client.client.CopyObjectPartDo(ctx, srcBucket, srcKey, bucket.Name, key, uploadId, partNumber, srcOffset, srcLength, nil) + result, err := bucket.client.S3Client().CopyObjectPartDo(ctx, srcBucket, srcKey, bucket.Name, key, uploadId, partNumber, srcOffset, srcLength, nil) if err != nil { return "", errors.Wrap(err, "CopyObjectPartDo") } diff --git a/pkg/multicloud/objectstore/ceph/adminapi.go b/pkg/multicloud/objectstore/ceph/adminapi.go new file mode 100644 index 0000000000..01e255e54b --- /dev/null +++ b/pkg/multicloud/objectstore/ceph/adminapi.go @@ -0,0 +1,284 @@ +// 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 ceph + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/tristate" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/util/httputils" + "yunion.io/x/onecloud/pkg/util/s3auth" +) + +type SCephAdminApi struct { + adminPath string + accessKey string + secret string + endpoint string + client *http.Client + debug bool +} + +func newCephAdminApi(ak, sk, ep string, debug bool, adminPath string) *SCephAdminApi { + if adminPath == "" { + adminPath = "admin" + } + return &SCephAdminApi{ + adminPath: adminPath, + accessKey: ak, + secret: sk, + endpoint: ep, + client: httputils.GetDefaultClient(), + debug: debug, + } +} + +func getJsonBodyReader(body jsonutils.JSONObject) io.Reader { + var reqBody io.Reader + if body != nil { + reqBody = strings.NewReader(body.String()) + } + return reqBody +} + +func (api *SCephAdminApi) jsonRequest(ctx context.Context, method httputils.THttpMethod, path string, hdr http.Header, body jsonutils.JSONObject) (http.Header, jsonutils.JSONObject, error) { + urlStr := strings.TrimRight(api.endpoint, "/") + "/" + strings.TrimLeft(path, "/") + req, err := http.NewRequest(string(method), urlStr, getJsonBodyReader(body)) + if err != nil { + return nil, nil, errors.Wrap(err, "http.NewRequest") + } + if hdr != nil { + for k, vs := range hdr { + for _, v := range vs { + req.Header.Add(k, v) + } + } + } + + newReq := s3auth.SignV4(*req, api.accessKey, api.secret, "cn-beijing", getJsonBodyReader(body)) + + resp, err := api.client.Do(newReq) + + return httputils.ParseJSONResponse(resp, err, api.debug) +} + +func (api *SCephAdminApi) GetUsage(ctx context.Context, uid string) (jsonutils.JSONObject, error) { + path := fmt.Sprintf("/%s/usage?format=json&uid=%s&show-entries=False&show-summary=True", api.adminPath, uid) + _, resp, err := api.jsonRequest(ctx, httputils.GET, path, nil, nil) + return resp, err +} + +// {"caps":[{"perm":"*","type":"buckets"},{"perm":"*","type":"usage"},{"perm":"*","type":"users"}], +// "display_name":"First User","email":"", +// "keys":[{"access_key":"70TTA6IU5D9LQ0IVA3Z6","secret_key":"AK3Kd9L1elPin9wEXNRuY9L2yxZ5U3mGsGYoyIxL","user":"testuser"}], +// "max_buckets":1000,"subusers":[],"suspended":0,"swift_keys":[],"tenant":"","user_id":"testuser"} + +type SUserInfo struct { + Caps []SUserCapability + DisplayName string + Email string + UserId string + Tenant string + Suspended int + SubUsers []string + MaxBuckets int + Keys []SUserAccessKey +} + +type SUserCapability struct { + Perm string + Type string +} + +type SUserAccessKey struct { + AccessKey string + SecretKey string + User string +} + +func (api *SCephAdminApi) GetUserInfo(ctx context.Context, uid string) (*SUserInfo, error) { + path := fmt.Sprintf("/%s/user?format=json&uid=%s", api.adminPath, uid) + _, resp, err := api.jsonRequest(ctx, httputils.GET, path, nil, nil) + if err != nil { + if httputils.ErrorCode(err) == 403 { + msg := `add users read cap by: radosgw-admin caps add --uid %s --caps="users=read"` + return nil, errors.Wrapf(httperrors.ErrForbidden, msg, uid) + } + return nil, errors.Wrap(err, "api.jsonRequest") + } + usrInfo := SUserInfo{} + err = resp.Unmarshal(&usrInfo) + if err != nil { + return nil, errors.Wrap(err, "resp.Unmarshal") + } + return &usrInfo, nil +} + +type SQuotaQuery struct { + QuotaType string `json:"quota-type"` + Uid string `json:"uid"` + Bucket string `json:"bucket"` +} + +type SQuota struct { + Enabled tristate.TriState `json:"enabled"` + CheckOnRaw bool `json:"check_on_raw,omitfalse"` + MaxSize int64 `json:"max_size,omitzero"` + MaxSizeKB int64 `json:"max_size_kb,omitzero"` + MaxObjects int `json:"max_objects,omitzero"` +} + +func (q SQuotaQuery) Query() string { + return "quota&format=json&" + jsonutils.Marshal(q).QueryString() +} + +func (q *SQuotaQuery) SetBucket(uid string, level string, bucket string) { + q.Uid = uid + q.Bucket = bucket + q.QuotaType = level +} + +func (api *SCephAdminApi) GetUserQuota(ctx context.Context, uid string) (*SQuota, *SQuota, error) { + userQuota, err := api.getQuota(ctx, uid, "user", "") + if err != nil { + return nil, nil, errors.Wrap(err, "api.getQuota user level") + } + bucketQuota, err := api.getQuota(ctx, uid, "bucket", "") + if err != nil { + return nil, nil, errors.Wrap(err, "api.getQuota bucket level") + } + return userQuota, bucketQuota, nil +} + +// ceph目前不支持设置bucket的quota,因此返回全局的bucket quota +func (api *SCephAdminApi) GetBucketQuota(ctx context.Context, uid string, bucket string) (*SQuota, error) { + /*quota, err := api.getQuota(ctx, uid, "", bucket) + if err == nil { + return quota, nil + }*/ + return api.getQuota(ctx, uid, "bucket", "") +} + +func (api *SCephAdminApi) getQuota(ctx context.Context, uid string, level string, bucket string) (*SQuota, error) { + query := SQuotaQuery{} + query.SetBucket(uid, level, bucket) + var path string + if len(bucket) > 0 { + path = fmt.Sprintf("/%s/bucket?%s", api.adminPath, query.Query()) + } else { + path = fmt.Sprintf("/%s/user?%s", api.adminPath, query.Query()) + } + _, resp, err := api.jsonRequest(ctx, httputils.GET, path, nil, nil) + if err != nil { + if httputils.ErrorCode(err) == 403 { + var msg string + if len(bucket) > 0 { + msg = `add buckets read cap by: radosgw-admin caps add --uid %s --caps="buckets=read"` + } else { + msg = `add users read cap by: radosgw-admin caps add --uid %s --caps="users=read"` + } + return nil, errors.Wrapf(httperrors.ErrForbidden, msg, uid) + } + return nil, errors.Wrap(err, "api.jsonRequest") + } + if resp == nil { + return nil, httperrors.ErrNotSupported + } + quota := SQuota{} + err = resp.Unmarshal("a) + if err != nil { + return nil, errors.Wrap(err, "resp.Unmarshal") + } + return "a, nil +} + +func (api *SCephAdminApi) SetUserQuota(ctx context.Context, uid string, sizeBytes int64, objects int) error { + _, err := api.setQuota(ctx, uid, "user", "", sizeBytes, objects) + return errors.Wrap(err, "api.setQuota") +} + +func (api *SCephAdminApi) SetAllBucketQuota(ctx context.Context, uid string, sizeBytes int64, objects int) error { + _, err := api.setQuota(ctx, uid, "bucket", "", sizeBytes, objects) + return errors.Wrap(err, "api.setQuota") +} + +// ceph目前不支持设置quota,因此返回全局的bucket quota +func (api *SCephAdminApi) SetBucketQuota(ctx context.Context, uid string, bucket string, sizeBytes int64, objects int) error { + var err error + _, err = api.setQuota(ctx, uid, "", bucket, sizeBytes, objects) + if err == nil { + return nil + } + _, err = api.setQuota(ctx, uid, "bucket", "", sizeBytes, objects) + if err == nil { + return nil + } + _, err = api.setQuota(ctx, uid, "user", "", sizeBytes, objects) + if err == nil { + return nil + } + return errors.Wrap(err, "api.setQuota") +} + +func (api *SCephAdminApi) setQuota(ctx context.Context, uid string, level string, bucket string, sizeBytes int64, objects int) (*SQuota, error) { + query := SQuotaQuery{} + query.SetBucket(uid, level, bucket) + quota := SQuota{ + MaxSize: sizeBytes, + MaxObjects: objects, + } + if sizeBytes <= 0 && objects <= 0 { + quota.Enabled = tristate.False + } else { + quota.Enabled = tristate.True + } + var path string + if len(bucket) > 0 { + path = fmt.Sprintf("/%s/bucket?%s", api.adminPath, query.Query()) + } else { + path = fmt.Sprintf("/%s/user?%s", api.adminPath, query.Query()) + } + body := jsonutils.Marshal(quota) + log.Debugf("request %s %s %s", httputils.PUT, path, body) + _, resp, err := api.jsonRequest(ctx, httputils.PUT, path, nil, body) + if err != nil { + if httputils.ErrorCode(err) == 403 { + var msg string + if len(bucket) > 0 { + msg = `add buckets write cap by: radosgw-admin caps add --uid %s --caps="buckets=write"` + } else { + msg = `add users write cap by: radosgw-admin caps add --uid %s --caps="users=write"` + } + return nil, errors.Wrapf(httperrors.ErrForbidden, msg, uid) + } + return nil, errors.Wrap(err, "api.jsonRequest") + } + log.Debugf("%s", resp) + quota = SQuota{} + err = resp.Unmarshal("a) + if err != nil { + return nil, errors.Wrap(err, "resp.Unmarshal") + } + return "a, nil +} diff --git a/pkg/multicloud/objectstore/ceph/bucket.go b/pkg/multicloud/objectstore/ceph/bucket.go new file mode 100644 index 0000000000..9c9d75347b --- /dev/null +++ b/pkg/multicloud/objectstore/ceph/bucket.go @@ -0,0 +1,67 @@ +// 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 ceph + +import ( + "context" + "strconv" + + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/multicloud/objectstore" +) + +type SCephRadosBucket struct { + *objectstore.SBucket +} + +func (b *SCephRadosBucket) GetStats() cloudprovider.SBucketStats { + _, hdr, _ := b.GetIBucketProvider().S3Client().BucketExists(b.Name) + if hdr != nil { + sizeBytesStr := hdr.Get("X-Rgw-Bytes-Used") + sizeBytes, _ := strconv.ParseInt(sizeBytesStr, 10, 64) + objCntStr := hdr.Get("X-Rgw-Object-Count") + objCnt, _ := strconv.ParseInt(objCntStr, 10, 64) + return cloudprovider.SBucketStats{ + SizeBytes: sizeBytes, + ObjectCount: int(objCnt), + } + } + return b.SBucket.GetStats() +} + +func (b *SCephRadosBucket) GetLimit() cloudprovider.SBucketStats { + if cephCli, ok := b.GetIBucketProvider().(*SCephRadosClient); ok { + quota, err := cephCli.adminApi.GetBucketQuota(context.Background(), cephCli.GetAccountId(), b.Name) + if err == nil { + limit := cloudprovider.SBucketStats{} + if quota.Enabled.IsTrue() { + limit.SizeBytes = quota.MaxSize + limit.ObjectCount = quota.MaxObjects + } + return limit + } + } + return b.SBucket.GetLimit() +} + +func (b *SCephRadosBucket) SetLimit(limit cloudprovider.SBucketStats) error { + /*if cephCli, ok := b.GetIBucketProvider().(*SCephRadosClient); ok { + err := cephCli.adminApi.SetBucketQuota(context.Background(), cephCli.GetAccountId(), b.Name, limit.SizeBytes, limit.ObjectCount) + return errors.Wrap(err, "cephCli.adminApi.SetBucketQuota") + } + return b.SBucket.SetLimit(limit)*/ + return httperrors.ErrNotSupported +} diff --git a/pkg/multicloud/objectstore/ceph/ceph.go b/pkg/multicloud/objectstore/ceph/ceph.go new file mode 100644 index 0000000000..eba3013382 --- /dev/null +++ b/pkg/multicloud/objectstore/ceph/ceph.go @@ -0,0 +1,115 @@ +// 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 ceph + +import ( + "context" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/s3cli" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/multicloud/objectstore" +) + +type SCephRadosClient struct { + *objectstore.SObjectStoreClient + + adminApi *SCephAdminApi + + userQuota *SQuota + bucketQuota *SQuota + userInfo *SUserInfo +} + +func NewCephRados(providerId string, providerName string, endpoint string, accessKey string, secret string, isDebug bool) (*SCephRadosClient, error) { + s3store, err := objectstore.NewObjectStoreClientAndFetch(providerId, providerName, endpoint, accessKey, secret, isDebug, false) + if err != nil { + return nil, errors.Wrap(err, "NewObjectStoreClient") + } + adminApi := newCephAdminApi(accessKey, secret, endpoint, isDebug, "") + + client := SCephRadosClient{ + SObjectStoreClient: s3store, + adminApi: adminApi, + } + + client.SetVirtualObject(&client) + + err = client.FetchBuckets() + if err != nil { + return nil, errors.Wrap(err, "fetchBuckets") + } + + userQuota, bucketQuota, err := adminApi.GetUserQuota(context.Background(), s3store.GetAccountId()) + if err != nil { + if errors.Cause(err) != httperrors.ErrForbidden { + return nil, errors.Wrap(err, "adminApi.GetUserQuota") + } else { + // skip the error + log.Errorf("adminApi.GetUserQuota fail: %s", err) + } + } + userInfo, err := adminApi.GetUserInfo(context.Background(), s3store.GetAccountId()) + if err != nil { + if errors.Cause(err) != httperrors.ErrForbidden { + return nil, errors.Wrap(err, "adminApi.GetUserInfo") + } else { + // skip the error + log.Errorf("adminApi.GetUserInfo fail: %s", err) + } + } + if isDebug { + log.Debugf("%#v %#v %#v", userQuota, bucketQuota, userInfo) + } + client.userQuota = userQuota + client.bucketQuota = bucketQuota + client.userInfo = userInfo + + return &client, nil +} + +func (cli *SCephRadosClient) GetVersion() string { + return "" +} + +func (cli *SCephRadosClient) About() jsonutils.JSONObject { + about := jsonutils.NewDict() + if cli.userQuota != nil { + about.Add(jsonutils.Marshal(cli.userQuota), "user_quota") + } + if cli.bucketQuota != nil { + about.Add(jsonutils.Marshal(cli.bucketQuota), "bucket_quota") + } + if cli.userInfo != nil { + about.Add(jsonutils.Marshal(cli.userInfo), "user_info") + } + return about +} + +func (cli *SCephRadosClient) GetProvider() string { + return api.CLOUD_PROVIDER_CEPH +} + +func (cli *SCephRadosClient) NewBucket(bucket s3cli.BucketInfo) cloudprovider.ICloudBucket { + generalBucket := cli.SObjectStoreClient.NewBucket(bucket) + return &SCephRadosBucket{ + SBucket: generalBucket.(*objectstore.SBucket), + } +} diff --git a/pkg/multicloud/objectstore/ceph/provider/provider.go b/pkg/multicloud/objectstore/ceph/provider/provider.go new file mode 100644 index 0000000000..af9e17865c --- /dev/null +++ b/pkg/multicloud/objectstore/ceph/provider/provider.go @@ -0,0 +1,56 @@ +// 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 provider + +import ( + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudprovider" + "yunion.io/x/onecloud/pkg/multicloud/objectstore/ceph" + s3provider "yunion.io/x/onecloud/pkg/multicloud/objectstore/provider" +) + +type SCephRadosProviderFactory struct { + s3provider.SObjectStoreProviderFactory +} + +func (self *SCephRadosProviderFactory) GetId() string { + return api.CLOUD_PROVIDER_CEPH +} + +func (self *SCephRadosProviderFactory) GetName() string { + return api.CLOUD_PROVIDER_CEPH +} + +func (self *SCephRadosProviderFactory) GetProvider(providerId, providerName, url, account, secret string) (cloudprovider.ICloudProvider, error) { + client, err := ceph.NewCephRados(providerId, providerName, url, account, secret, false) + if err != nil { + return nil, err + } + return s3provider.NewObjectStoreProvider(self, client), nil +} + +func (self *SCephRadosProviderFactory) GetClientRC(url, account, secret string) (map[string]string, error) { + return map[string]string{ + "S3_ACCESS_KEY": account, + "S3_SECRET": secret, + "S3_ACCESS_URL": url, + "S3_BACKEND": api.CLOUD_PROVIDER_CEPH, + }, nil +} + +func init() { + factory := SCephRadosProviderFactory{} + cloudprovider.RegisterFactory(&factory) +} diff --git a/pkg/multicloud/objectstore/interface.go b/pkg/multicloud/objectstore/interface.go new file mode 100644 index 0000000000..0323b894d5 --- /dev/null +++ b/pkg/multicloud/objectstore/interface.go @@ -0,0 +1,42 @@ +// 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 objectstore + +import ( + "yunion.io/x/jsonutils" + "yunion.io/x/s3cli" + + "yunion.io/x/onecloud/pkg/cloudprovider" +) + +type IBucketProvider interface { + cloudprovider.ICloudRegion + + NewBucket(bucket s3cli.BucketInfo) cloudprovider.ICloudBucket + + GetEndpoint() string + + S3Client() *s3cli.Client + + About() jsonutils.JSONObject + GetVersion() string + GetAccountId() string + GetSubAccounts() ([]cloudprovider.SSubAccount, error) + + GetObjectAcl(bucket, key string) (cloudprovider.TBucketACLType, error) + SetObjectAcl(bucket, key string, cannedAcl cloudprovider.TBucketACLType) error + GetIBucketAcl(name string) (cloudprovider.TBucketACLType, error) + SetIBucketAcl(name string, cannedAcl cloudprovider.TBucketACLType) error +} diff --git a/pkg/multicloud/objectstore/objectstore.go b/pkg/multicloud/objectstore/objectstore.go index 791a5b8122..9b5089e29e 100644 --- a/pkg/multicloud/objectstore/objectstore.go +++ b/pkg/multicloud/objectstore/objectstore.go @@ -54,6 +54,10 @@ type SObjectStoreClient struct { } func NewObjectStoreClient(providerId string, providerName string, endpoint string, accessKey string, secret string, isDebug bool) (*SObjectStoreClient, error) { + return NewObjectStoreClientAndFetch(providerId, providerName, endpoint, accessKey, secret, isDebug, true) +} + +func NewObjectStoreClientAndFetch(providerId string, providerName string, endpoint string, accessKey string, secret string, isDebug bool, doFetch bool) (*SObjectStoreClient, error) { client := SObjectStoreClient{ providerId: providerId, providerName: providerName, @@ -79,14 +83,15 @@ func NewObjectStoreClient(providerId string, providerName string, endpoint strin cli.SetCustomTransport(tr) client.client = cli + client.SetVirtualObject(&client) - err = client.fetchBuckets() - if err != nil { - return nil, errors.Wrap(err, "fetchBuckets") + if doFetch { + err = client.FetchBuckets() + if err != nil { + return nil, errors.Wrap(err, "fetchBuckets") + } } - log.Debugf("clientID: %s Name: %s", client.ownerId, client.ownerName) - return &client, nil } @@ -107,6 +112,10 @@ func (cli *SObjectStoreClient) GetIRegion() cloudprovider.ICloudRegion { return cli.GetVirtualObject().(cloudprovider.ICloudRegion) } +func (cli *SObjectStoreClient) GetIBucketProvider() IBucketProvider { + return cli.GetVirtualObject().(IBucketProvider) +} + func (cli *SObjectStoreClient) GetVersion() string { return "" } @@ -120,6 +129,24 @@ func (cli *SObjectStoreClient) GetProvider() string { return api.CLOUD_PROVIDER_GENERICS3 } +////////////////////////////// IBucketProvider ////////////////////////////// + +func (cli *SObjectStoreClient) NewBucket(bucket s3cli.BucketInfo) cloudprovider.ICloudBucket { + return &SBucket{ + client: cli.GetIBucketProvider(), + Name: bucket.Name, + CreatedAt: bucket.CreationDate, + } +} + +func (cli *SObjectStoreClient) GetEndpoint() string { + return cli.endpoint +} + +func (cli *SObjectStoreClient) S3Client() *s3cli.Client { + return cli.client +} + ///////////////////////////////// fake impletementations ////////////////////// func (cli *SObjectStoreClient) GetIZones() ([]cloudprovider.ICloudZone, error) { @@ -286,7 +313,7 @@ func (self *SObjectStoreClient) invalidateIBuckets() { func (self *SObjectStoreClient) getIBuckets() ([]cloudprovider.ICloudBucket, error) { if self.iBuckets == nil { - err := self.fetchBuckets() + err := self.FetchBuckets() if err != nil { return nil, errors.Wrap(err, "fetchBuckets") } @@ -294,7 +321,7 @@ func (self *SObjectStoreClient) getIBuckets() ([]cloudprovider.ICloudBucket, err return self.iBuckets, nil } -func (cli *SObjectStoreClient) fetchBuckets() error { +func (cli *SObjectStoreClient) FetchBuckets() error { result, err := cli.client.ListBuckets() if err != nil { return errors.Wrap(err, "client.ListBuckets") @@ -304,12 +331,8 @@ func (cli *SObjectStoreClient) fetchBuckets() error { buckets := result.Buckets.Bucket cli.iBuckets = make([]cloudprovider.ICloudBucket, len(buckets)) for i := range buckets { - b := SBucket{ - client: cli, - Name: buckets[i].Name, - CreatedAt: buckets[i].CreationDate, - } - cli.iBuckets[i] = &b + b := cli.GetIBucketProvider().NewBucket(buckets[i]) + cli.iBuckets[i] = b } return nil } diff --git a/pkg/multicloud/objectstore/provider/provider.go b/pkg/multicloud/objectstore/provider/provider.go index b7709fe5e5..9a452868dd 100644 --- a/pkg/multicloud/objectstore/provider/provider.go +++ b/pkg/multicloud/objectstore/provider/provider.go @@ -82,11 +82,7 @@ func (self *SObjectStoreProviderFactory) GetProvider(providerId, providerName, u if err != nil { return nil, err } - client.SetVirtualObject(client) - return &SObjectStoreProvider{ - SBaseProvider: cloudprovider.NewBaseProvider(self), - client: client, - }, nil + return NewObjectStoreProvider(self, client), nil } func (self *SObjectStoreProviderFactory) GetClientRC(url, account, secret string) (map[string]string, error) { @@ -94,6 +90,7 @@ func (self *SObjectStoreProviderFactory) GetClientRC(url, account, secret string "S3_ACCESS_KEY": account, "S3_SECRET": secret, "S3_ACCESS_URL": url, + "S3_BACKEND": api.CLOUD_PROVIDER_GENERICS3, }, nil } @@ -104,7 +101,14 @@ func init() { type SObjectStoreProvider struct { cloudprovider.SBaseProvider - client *objectstore.SObjectStoreClient + client objectstore.IBucketProvider +} + +func NewObjectStoreProvider(factory cloudprovider.ICloudProviderFactory, client objectstore.IBucketProvider) *SObjectStoreProvider { + return &SObjectStoreProvider{ + SBaseProvider: cloudprovider.NewBaseProvider(factory), + client: client, + } } func (self *SObjectStoreProvider) GetIRegions() []cloudprovider.ICloudRegion { diff --git a/pkg/multicloud/objectstore/shell.go b/pkg/multicloud/objectstore/shell.go index 7751ea28ad..fce23fef49 100644 --- a/pkg/multicloud/objectstore/shell.go +++ b/pkg/multicloud/objectstore/shell.go @@ -80,6 +80,29 @@ func S3Shell() { return nil }) + type BucketLimitOptions struct { + NAME string `help:"name of bucket to set limit"` + SizeGB int `help:"limit of volumes in GB"` + Objects int `help:"limit of object count"` + Off bool `help:"Turn off limit"` + } + shellutils.R(&BucketLimitOptions{}, "bucket-set-limit", "Set bucket limit", func(cli cloudprovider.ICloudRegion, args *BucketLimitOptions) error { + bucket, err := cli.GetIBucketByName(args.NAME) + if err != nil { + return err + } + if args.Off { + err = bucket.SetLimit(cloudprovider.SBucketStats{}) + } else { + fmt.Println("set limit") + err = bucket.SetLimit(cloudprovider.SBucketStats{SizeBytes: int64(args.SizeGB * 1000 * 1000 * 1000), ObjectCount: args.Objects}) + } + if err != nil { + return err + } + return nil + }) + type BucketExistOptions struct { NAME string `help:"name of bucket to delete"` } diff --git a/pkg/s3gateway/handlers/middelware.go b/pkg/s3gateway/handlers/middelware.go index e8e4e97c62..3178cf7c69 100644 --- a/pkg/s3gateway/handlers/middelware.go +++ b/pkg/s3gateway/handlers/middelware.go @@ -20,6 +20,7 @@ import ( "yunion.io/x/pkg/gotypes" + "yunion.io/x/log" "yunion.io/x/onecloud/pkg/appctx" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/mcclient/auth" @@ -31,6 +32,8 @@ const ( func s3authenticate(f appsrv.FilterHandler) appsrv.FilterHandler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + log.Debugf("%s %s %s %s", r.Method, r.Host, r.URL, r.Header) + o, err := getObjectRequest(r) if err != nil { SendError(w, BadRequest(ctx, err.Error())) diff --git a/pkg/s3gateway/service/service.go b/pkg/s3gateway/service/service.go index dc2982e11a..527214d2e6 100644 --- a/pkg/s3gateway/service/service.go +++ b/pkg/s3gateway/service/service.go @@ -26,13 +26,7 @@ import ( "yunion.io/x/onecloud/pkg/s3gateway/handlers" "yunion.io/x/onecloud/pkg/s3gateway/options" - _ "yunion.io/x/onecloud/pkg/multicloud/aliyun/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/aws/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/azure/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/huawei/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/objectstore/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/qcloud/provider" - _ "yunion.io/x/onecloud/pkg/multicloud/ucloud/provider" + _ "yunion.io/x/onecloud/pkg/multicloud/loader" ) func StartService() { diff --git a/pkg/util/s3auth/v2.go b/pkg/util/s3auth/v2.go index 99522df343..e2f78603b6 100644 --- a/pkg/util/s3auth/v2.go +++ b/pkg/util/s3auth/v2.go @@ -231,9 +231,14 @@ func (aksk SAccessKeyRequestV2) Encode() string { return jsonutils.Marshal(aksk).String() } +func NewV2Request() SAccessKeyRequestV2 { + req := SAccessKeyRequestV2{} + req.Algorithm = signV2Algorithm + return req +} + func decodeAuthHeaderV2(authStr string) (*SAccessKeyRequestV2, error) { - akskReq := SAccessKeyRequestV2{} - akskReq.Algorithm = signV2Algorithm + akskReq := NewV2Request() pos := strings.IndexByte(authStr, ':') if pos <= 0 { return nil, errors.Error("illegal authorization header") diff --git a/pkg/util/s3auth/v4.go b/pkg/util/s3auth/v4.go index 4bd88b3851..5353346cc4 100644 --- a/pkg/util/s3auth/v4.go +++ b/pkg/util/s3auth/v4.go @@ -19,6 +19,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "io" "net/http" "sort" "strings" @@ -26,6 +27,9 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/util/streamutils" + "yunion.io/x/pkg/util/timeutils" ) // Signature and API related constants. @@ -188,13 +192,25 @@ func getCanonicalHeaders(req http.Request, signedHeaders []string) string { // request header names. func getSignedHeaders(req http.Request, ignoredHeaders map[string]bool) []string { var headers []string + hasHost := false + hasContentHash := false for k := range req.Header { if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok { continue // Ignored header found continue. } + if strings.EqualFold(k, "host") { + hasHost = true + } else if strings.EqualFold(k, "x-amz-content-sha256") { + hasContentHash = true + } headers = append(headers, strings.ToLower(k)) } - headers = append(headers, "host") + if !hasHost { + headers = append(headers, "host") + } + if !hasContentHash { + headers = append(headers, "x-amz-content-sha256") + } sort.Strings(headers) return headers } @@ -217,11 +233,16 @@ type SAccessKeyRequestV4 struct { SignDate time.Time } +func NewV4Request() SAccessKeyRequestV4 { + req := SAccessKeyRequestV4{} + req.Algorithm = signV4Algorithm + return req +} + // AWS4-HMAC-SHA256 // Credential=xxxx/20190824/us-east-1/s3/aws4_request,SignedHeaders=date;host;x-amz-content-sha256;x-amz-date,Signature=27a135c6f51cc func decodeAuthHeaderV4(authStr string) (*SAccessKeyRequestV4, error) { - req := SAccessKeyRequestV4{} - req.Algorithm = signV4Algorithm + req := NewV4Request() parts := strings.Split(authStr, ",") if len(parts) != 3 || !strings.HasPrefix(parts[0], "Credential=") || @@ -268,3 +289,60 @@ func (aksk SAccessKeyRequestV4) Verify(secret string) error { func (aksk SAccessKeyRequestV4) Encode() string { return jsonutils.Marshal(aksk).String() } + +// GetCredential generate a credential string. +func getCredential(accessKeyID, location string, t time.Time) string { + scope := getScope(location, t) + return accessKeyID + "/" + scope +} + +// SignV4 sign the request before Do(), in accordance with +// http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html. +func SignV4(req http.Request, accessKey, secretAccessKey, location string, body io.Reader) *http.Request { + // Signature calculation is not needed for anonymous credentials. + if accessKey == "" || secretAccessKey == "" { + return &req + } + + h := sha256.New() + if body != nil { + streamutils.StreamPipe(body, h, false) + } + req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(h.Sum(nil))) + + // Initial time. + t := time.Now().UTC() + + // Set x-amz-date. + req.Header.Set("X-Amz-Date", t.Format(iso8601DateFormat)) + req.Header.Set("Date", timeutils.RFC2882Time(t)) + + signedHeaders := getSignedHeaders(req, v4IgnoredHeaders) + // Get canonical request. + canonicalRequest := getCanonicalRequest(req, signedHeaders) + + // Get string to sign from canonical request. + stringToSign := getStringToSignV4(t, location, canonicalRequest) + + // Get hmac signing key. + signingKey := getSigningKey(secretAccessKey, location, t) + + // Get credential string. + credential := getCredential(accessKey, location, t) + + // Calculate signature. + signature := getSignature(signingKey, stringToSign) + + // If regular request, construct the final authorization header. + parts := []string{ + signV4Algorithm + " Credential=" + credential, + "SignedHeaders=" + strings.Join(signedHeaders, ";"), + "Signature=" + signature, + } + + // Set authorization header. + auth := strings.Join(parts, ",") + req.Header.Set("Authorization", auth) + + return &req +} diff --git a/pkg/util/s3auth/v4_test.go b/pkg/util/s3auth/v4_test.go new file mode 100644 index 0000000000..b378559872 --- /dev/null +++ b/pkg/util/s3auth/v4_test.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 s3auth + +import ( + "io" + "net/http" + "strings" + "testing" +) + +func TestSignV4(t *testing.T) { + getReq, _ := http.NewRequest(http.MethodGet, "https://api.aliyun-cs.com/2018-06-23/DescribeInstances?limit=10&offset=0", nil) + putBody := "<123>" + putReq, _ := http.NewRequest(http.MethodPut, "https://api.aliyun-cs.com/2018-06-23/ModifyInstance?InstanceId=aabbccdd", strings.NewReader(putBody)) + + cases := []struct { + request http.Request + accessKey string + secret string + location string + body io.Reader + }{ + { + request: *getReq, + accessKey: "1234567890", + secret: "1234567890", + location: "cn-beijing", + body: nil, + }, + { + request: *putReq, + accessKey: "1234567890", + secret: "1234567890", + location: "cn-beijing", + body: strings.NewReader(putBody), + }, + } + + for _, c := range cases { + newreq := SignV4(c.request, c.accessKey, c.secret, c.location, c.body) + t.Logf("%#v", newreq) + + req, err := DecodeAccessKeyRequest(*newreq, false) + if err != nil { + t.Errorf("DecodeAccessKeyRequest fail %s", err) + continue + } + + err = req.Verify(c.secret) + if err != nil { + t.Errorf("verify fail %s", err) + } + } + +}