feature: 1. add ceph cloud provider based on generic s3 provider 2.

climc support AK/SK authentication
This commit is contained in:
Qiu Jian
2019-09-01 01:58:43 +08:00
parent d884992399
commit 2f90f35060
26 changed files with 998 additions and 83 deletions
+28 -18
View File
@@ -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
}
+11
View File
@@ -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"`
+7 -3
View File
@@ -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 {
+9 -2
View File
@@ -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)
+1
View File
@@ -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" // 不足按需资源余额
+14
View File
@@ -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"
+1 -1
View File
@@ -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"
+28
View File
@@ -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
}
+2 -10
View File
@@ -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() {
+37
View File
@@ -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
}
+1 -1
View File
@@ -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
+42
View File
@@ -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 ...")
}
+24 -17
View File
@@ -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")
}
+284
View File
@@ -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(&quota)
if err != nil {
return nil, errors.Wrap(err, "resp.Unmarshal")
}
return &quota, 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(&quota)
if err != nil {
return nil, errors.Wrap(err, "resp.Unmarshal")
}
return &quota, nil
}
+67
View File
@@ -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
}
+115
View File
@@ -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),
}
}
@@ -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)
}
+42
View File
@@ -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
}
+36 -13
View File
@@ -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
}
@@ -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 {
+23
View File
@@ -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"`
}
+3
View File
@@ -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()))
+1 -7
View File
@@ -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() {
+7 -2
View File
@@ -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")
+81 -3
View File
@@ -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
}
+68
View File
@@ -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 := "<xml><123></123></xml>"
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)
}
}
}