Compare commits

..

157 Commits

Author SHA1 Message Date
Aditya Manthramurthy 892a204013 Update console to v0.15.8 (#14671) 2022-03-31 20:41:39 -07:00
Poorna 0e6aedc7ed Capture cmdline args for inspect API (#14668)
Co-authored-by: Poorna Krishnamoorthy <poorna@minio.io>
2022-03-31 16:05:43 -07:00
Naveen c547a4d835 Pin actions to a full length commit SHA (#14590)
- Pinned actions by SHA https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies
- Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions

https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions

Also, dependabot supports upgrades based on SHA.
2022-03-31 10:12:53 -07:00
Aditya Manthramurthy fc9668baa5 Increase IAM refresh rate to every 10 mins (#14661)
Add timing information for IAM init and refresh
2022-03-30 17:02:59 -07:00
Andreas Auernhammer ba17d46f15 ListObjectParts: simplify ETag decryption and size adjustment (#14653)
This commit simplifies the ETag decryption and size adjustment
when listing object parts.

When listing object parts, MinIO has to decrypt the ETag of all
parts if and only if the object resp. the parts is encrypted using
SSE-S3.
In case of SSE-KMS and SSE-C, MinIO returns a pseudo-random ETag.
This is inline with AWS S3 behavior.

Further, MinIO has to adjust the size of all encrypted parts due to
the encryption overhead.

The ListObjectParts does specifically not use the KMS bulk decryption
API (4d2fc530d0) since the ETags of all
parts are encrypted using the same object encryption key. Therefore,
MinIO only has to connect to the KMS once, even if there are multiple
parts resp. ETags. It can simply reuse the same object encryption key.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-30 15:23:25 -07:00
Harshavardhana 54a4f93854 update CREDITS 2022-03-30 14:09:39 -07:00
Krishna Srinivas bdd816488d Get the BackendInfo to fill the apporpriate struct fields (#14660) 2022-03-30 10:48:35 -07:00
Krishna Srinivas 36dcfee2f7 Allow decomission of pool even if a drive in it is down (#14656) 2022-03-29 22:51:31 -07:00
Poorna 4d13ddf6b3 Avoid shadowing error during replication proxy check (#14655)
Fixes #14652
2022-03-29 10:53:09 -07:00
Poorna 9e25475475 Validate tier manager is initialized in tier Empty() check (#14646)
Co-authored-by: Poorna Krishnamoorthy <poorna@minio.io>
2022-03-29 10:10:06 -07:00
Andreas Auernhammer e955aa7f2a kes: add support for encrypted private keys (#14650)
This commit adds support for encrypted KES
client private keys.

Now, it is possible to encrypt the KES client
private key (`MINIO_KMS_KES_KEY_FILE`) with
a password.

For example, KES CLI already supports the
creation of encrypted private keys:
```
kes identity new --encrypt --key client.key --cert client.crt MinIO
```

To decrypt an encrypted private key, the password
needs to be provided:
```
MINIO_KMS_KES_KEY_PASSWORD=<password>
```

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-29 09:53:33 -07:00
Eco 81d2b54dfd doc: typo fix for ttfb entry in table (#14647) 2022-03-29 09:42:02 -07:00
Harshavardhana 7956ff0313 fix: multiple pool setup return incorrect DeleteMarker metadata (#14642) 2022-03-27 23:39:50 -07:00
Aditya Manthramurthy 9ff25fb64b Load IAM in-memory cache using only a single list call (#14640)
- Increase global IAM refresh interval to 30 minutes
- Also print a log after loading IAM subsystem
2022-03-27 18:48:01 -07:00
Andreas Auernhammer 04df69f633 listing: decrypt only SSE-S3 single-part ETags (#14638)
This commit optimises the ETag decryption when
listing objects.

When MinIO lists objects, it has to decrypt the
ETags of single-part SSE-S3 objects.

It does not need to decrypt ETags of
 - plaintext objects => Their ETag is not encrypted
 - SSE-C objects     => Their ETag is not the content MD5
 - SSE-KMS objects   => Their ETag is not the content MD5
 - multipart objects => Their ETag is not encrypted

Hence, MinIO only needs to make a call to the KMS
when it needs to decrypt a single-part SSE-S3 object.
It can resolve the ETags off all other object types
locally.

This commit implements the above semantics by
processing an object listing in batches.
If the batch contains no single-part SSE-S3 object,
then no KMS calls will be made.

If the batch contains at least one single-part
SSE-S3 object we have to make at least one KMS call.
No we first filter all single-part SSE-S3 objects
such that we only request the decryption keys for
these objects.
Once we know which objects resp. ETags require a
decryption key, MinIO either uses the KES bulk
decryption API (if supported) or decrypts each
ETag serially.

This commit is a significant improvement compared
to the previous listing code. Before, a single
non-SSE-S3 object caused MinIO to fall-back to
a serial ETag decryption.
For example, if a batch consisted of 249 SSE-S3
objects and one single SSE-KMS object, MinIO would
send 249 requests to the KMS.
Now, MinIO will send a single request for exactly
those 249 objects and skip the one SSE-KMS object
since it can handle its ETag locally.

Further, MinIO would request decryption keys
for SSE-S3 multipart objects in the past - even
though multipart ETags are not encrypted.
So, if a bucket contained only multipart SSE-S3
objects, MinIO would make totally unnecessary
requests to the KMS.
Now, MinIO simply skips these multipart objects
since it can handle the ETags locally.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-27 18:34:11 -07:00
Anis Elleuch 908eb57795 Always get the actual object size (#14637)
In bulk ETag decryption, do not rely on the etag to check if it is
encrypted or not to decide if we should set the actual object size in
ObjectInfo. The reason is that multipart objects ETags are not
encrypted.

Always get the actual object size in that case.
2022-03-27 08:54:25 -07:00
Harshavardhana ecfae074dc do not crash when KMS is not enabled (#14634)
KMS when not enabled might crash when listing
an object that previously had SSE-S3 enabled,
fail appropriately in such situations.
2022-03-27 08:54:01 -07:00
Minio Trusted be5d394e56 Update yaml files to latest version RELEASE.2022-03-26T06-49-28Z 2022-03-26 07:32:25 +00:00
Minio Trusted 849a27ee61 update hotfixes instructions and fix some typo 2022-03-25 23:49:28 -07:00
Andreas Auernhammer 062f3ea43a etag: fix incorrect multipart detection (#14631)
This commit fixes a subtle bug in the ETag
`IsEncrypted` implementation.

An encrypted ETag may contain random bytes,
i.e. some randomness used for encryption.
This random value can contain a '-' byte
simple due to being randomly generated.

Before, the `IsEncrypted` implementation
incorrectly assumed that an encrypted ETag
cannot contain a '-' since it would be a
multipart ETag. Multipart ETags have a
16 byte value followed by a '-' and the part number.
For example:
```
059ba80b807c3c776fb3bcf3f33e11ae-2
```

However, the following encrypted ETag
```
20000f00db2d90a7b40782d4cff2b41a7799fc1e7ead25972db65150118dfbe2ba76a3c002da28f85c840cd2001a28a9
```
also contains a '-' byte but is not a multipart ETag.

This commit fixes the `IsEncrypted` implementation
simply by checking whether the ETag is at least 32
bytes long. A valid multipart ETag is never 32 bytes
long since a part number must be <= 10000.

However, an encrypted ETag must be at least 32 bytes
long. It contains the encrypted ETag bytes (16 bytes)
and the authentication tag added by the AEAD cipher (again
16 bytes).

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-25 18:21:01 -07:00
Harshavardhana 5cfedcfe33 askDisks for strict quorum to be equal to read quorum (#14623) 2022-03-25 16:29:45 -07:00
Andreas Auernhammer 4d2fc530d0 add support for SSE-S3 bulk ETag decryption (#14627)
This commit adds support for bulk ETag
decryption for SSE-S3 encrypted objects.

If KES supports a bulk decryption API, then
MinIO will check whether its policy grants
access to this API. If so, MinIO will use
a bulk API call instead of sending encrypted
ETags serially to KES.

Note that MinIO will not use the KES bulk API
if its client certificate is an admin identity.

MinIO will process object listings in batches.
A batch has a configurable size that can be set
via `MINIO_KMS_KES_BULK_API_BATCH_SIZE=N`.
It defaults to `500`.

This env. variable is experimental and may be
renamed / removed in the future.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-25 15:01:41 -07:00
Sergey Zhuk 3970204009 ci: Check for new go-version. Bump setup-go to v3 (#14598) 2022-03-25 08:56:04 -07:00
Harshavardhana f046f557fa request only 1 best version for latest version resolution (#14625)
ListObjects, ListObjectsV2 calls are being heavily taxed when
there are many versions on objects left over from a previous
release or ILM was never setup to clean them up. Instead
of being absolutely correct at resolving the exact latest
version of an object, we simply rely on the top most 1
version and resolve the rest.

Once we have obtained the top most "1" version for
ListObject, ListObjectsV2 call we break out.
2022-03-25 08:50:07 -07:00
Harshavardhana 401958938d add load balance properly restClientFromHash() bucket/prefix (#14621)
spread out resuming further to other nodes
2022-03-25 03:41:31 -07:00
Poorna 566cffe53d save format.json by default for inspect API (#14620) 2022-03-25 02:02:17 -07:00
Minio Trusted 028bc2f9be update console release to v0.15.6 2022-03-24 19:59:15 -07:00
Minio Trusted 813d9bc316 update helm release 2022-03-23 21:07:15 -07:00
Aditya Manthramurthy 79ba458051 fix: free up reader resources in S3Select properly (#14600) 2022-03-23 20:58:53 -07:00
Minio Trusted cf220be9b5 Update yaml files to latest version RELEASE.2022-03-24T00-43-44Z 2022-03-24 01:28:05 +00:00
Harshavardhana c433572585 update go mod to go1.16 deps (#14614) 2022-03-23 17:43:44 -07:00
Minio Trusted a42b576382 keep maximum concurrent operations to 512 (to sustain upto 1024 open fds) 2022-03-23 17:02:04 -07:00
Avimitin fb9b53026d Add riscv64 support (#14601)
In riscv64, the `syscall.Uname` function will return a uint8 slice.

    func main() {
      var buf syscall.Utsname
      fmt.Printf("Buffer Type: %T\n", buf.Release)
    }

    output:
      Buffer Type: [65]uint8

This is tested in the Arch Linux RISC-V 64 QEMU environment.

Signed-off-by: Avimitin <avimitin@gmail.com>
2022-03-22 20:36:59 -07:00
Klaus Post 2ac54e5a7b ListObjects: Filter lifecycle expired objects (#14606)
For ListObjects and ListObjectsV2 perform lifecycle checks on 
all objects before returning. This will filter out objects that are 
pending lifecycle expiration.

Bonus: Cheaper server pool conflict resolution by not converting to FileInfo.
2022-03-22 12:39:45 -07:00
Harshavardhana 8eecdc6d1f odd stripe sizes should choose (odd+1)/2 to get correct quorum (#14610) 2022-03-22 12:21:14 -07:00
Klaus Post 50577e2bd2 Allow adjusting request pool both ways (#14609)
When reloading a dynamic config allow the request pool to scale both ways.

Existing requests hold on to the previous pool, so they will pop the elements from that.
2022-03-22 11:28:54 -07:00
Klaus Post 7bc1f986e8 Do not wait for results when canceled (#14607)
When canceled nobody may be listening for the results.

Prevents memory buildup from cancelled requests.
2022-03-22 09:37:01 -07:00
Harshavardhana d796621ccc choose smaller default deadline for diagnostics without --full (#14599) 2022-03-21 23:25:24 -07:00
Minio Trusted 751e9fb7be Update yaml files to latest version RELEASE.2022-03-22T02-05-10Z 2022-03-22 02:45:24 +00:00
Harshavardhana f6113264f4 add detection for GOMAXPROCS < NumCPU 2022-03-21 19:05:10 -07:00
Harshavardhana a3534a730b fallback quorum should be "strict" globally if config is not loaded (#14589) 2022-03-20 17:39:06 -07:00
Minio Trusted 7f8b8a0e43 update console to v0.15.4 2022-03-20 15:35:20 -07:00
Harshavardhana bd6f7b6d83 fix: make decommission restart non-blocking (#14591)
currently an on-going decommission, during a server
restart might block the startup sequence for relatively
longer periods, instead start the decommission in
background lazily.
2022-03-20 14:46:43 -07:00
Andreas Auernhammer b0a4beb66a PutObjectPart: set SSE-KMS headers and truncate ETags. (#14578)
This commit fixes two bugs in the `PutObjectPartHandler`.
First, `PutObjectPart` should return SSE-KMS headers
when the object is encrypted using SSE-KMS.
Before, this was not the case.

Second, the ETag should always be a 16 byte hex string,
perhaps followed by a `-X` (where `X` is the number of parts).
However, `PutObjectPart` used to return the encrypted ETag
in case of SSE-KMS. This leaks MinIO internal etag details
through the S3 API.

The combination of both bugs causes clients that use SSE-KMS
to fail when trying to validate the ETag. Since `PutObjectPart`
did not send the SSE-KMS response headers, the response looked
like a plaintext `PutObjectPart` response. Hence, the client
tries to verify that the ETag is the content-md5 of the part.
This could never be the case, since MinIO used to return the
encrypted ETag.

Therefore, clients behaving as specified by the S3 protocol
tried to verify the ETag in a situation they should not.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-19 10:15:12 -07:00
Klaus Post 472c2d828c Fix waitgroup add after wait on config reload (#14584)
Fix `panic: "POST /minio/peer/v21/signalservice?signal=2": sync: WaitGroup is reused before previous Wait has returned`

Log entries already on the channel would cause `logEntry` to increment the
 waitgroup when sending messages, after Cancel has been called.

Instead of tracking every single message, just check the send goroutine. Faster 
and safe, since it will not decrement until the channel is closed.

Regression from #14289
2022-03-19 09:15:45 -07:00
Harshavardhana 01ee49045e fix: handle race in server setup global CI/CD variable (#14579) 2022-03-18 18:21:09 -07:00
Harshavardhana 7bd9f821dd return correct context errors for locking operations (#14569)
if a context is canceled do not need to return a timeout error
instead, return the appropriate error for context canceled.
2022-03-18 15:32:45 -07:00
Anis Elleuch b20ecc7b54 Add support of TLS session tickets with KES server (#14577)
Reduce overhead for communication between MinIO server and KES server.
2022-03-18 15:14:10 -07:00
Klaus Post 61eb9d4e29 Fix listing fallback re-using disks (#14576)
When more than 2 disks are unavailable for listing, the same disk will be used for fallback.

This makes quorum calculations incorrect since the same disk will have multiple entries.

This PR keeps track of which fallback disks have been handed out and only every returns a disk once.
2022-03-18 11:35:27 -07:00
Harshavardhana 43eb5a001c re-use transport for AdminInfo() call (#14571)
avoids creating new transport for each `isServerResolvable`
request, instead re-use the available global transport and do
not try to forcibly close connections to avoid TIME_WAIT
build upon large clusters.

Never use httpClient.CloseIdleConnections() since that can have
a drastic effect on existing connections on the transport pool.

Remove it everywhere.
2022-03-17 16:20:10 -07:00
Minio Trusted f58692abb7 update helm to v3.6.2 2022-03-17 11:30:55 -07:00
Klaus Post c1760fb764 Move apiCalls to front for field alignment (#14568)
Fixes #14565
2022-03-17 10:57:52 -07:00
Minio Trusted e9bc0e7e98 Update yaml files to latest version RELEASE.2022-03-17T06-34-49Z 2022-03-17 00:11:59 -07:00
Minio Trusted ffcadcd99e Revert "Use S3 client for uplooads/downloads during perf test (#14553)"
This reverts commit ff811f594b.

Speedtest is broken need to fix this more cleanly.
2022-03-16 23:34:49 -07:00
Minio Trusted 7a733a8d54 Update yaml files to latest version RELEASE.2022-03-17T02-57-36Z 2022-03-16 22:27:48 -07:00
Aditya Manthramurthy ce97313fda Add extra LDAP configuration validation (#14535)
- The result now contains suggestions on fixing common configuration issues.
- These suggestions will subsequently be exposed in console/mc
2022-03-16 19:57:36 -07:00
Krishnan Parthasarathi 7b81967a3c Fix handling of object versions pending purge (#14555)
- GetObject() with vid should return 405
- GetObject() without vid should return 404
- ListObjects() should ignore this object if this is the "latest" version of the object
- ListObjectVersions() should list this object as "DELETE marker"
- Remove data parts before sync'ing the version pending purge
2022-03-16 16:59:43 -07:00
Krishna Srinivas ff811f594b Use S3 client for uplooads/downloads during perf test (#14553) 2022-03-16 16:58:46 -07:00
Harshavardhana 0bf80b3c89 update console v0.15.3 2022-03-16 01:19:00 -07:00
Harshavardhana ae3b369fe1 logger webhook failure can overrun the queue_size (#14556)
PR introduced in #13819 was incorrect and was not
handling the situation where a buffer is full can
cause incessant amount of logs that would keep the
logger webhook overrun by the requests.

To avoid this only log failures to console logger
instead of all targets as it can cause self reference,
leading to an infinite loop.
2022-03-15 17:45:51 -07:00
Kourosh Tafreshi 77b15e7194 Add Console Service port to the NetworkPolicy (#14545) 2022-03-14 17:13:42 -07:00
Harshavardhana 20537f974e add missing v3.6.1 tarball 2022-03-14 17:13:17 -07:00
Harshavardhana 4476a64bdf update helm to v3.6.1 2022-03-14 14:40:24 -07:00
Steven Meyer d4b701576e Fix helm chart k8s version comparison (#14552) 2022-03-14 14:39:32 -07:00
Minio Trusted 721c053712 Update yaml files to latest version RELEASE.2022-03-14T18-25-24Z 2022-03-14 19:32:22 +00:00
Harshavardhana e3071157f0 allow MakeBucketLocation to work for metaBucket (#14548)
decommission would fail to start due to failure
in MakeBucketLocation() error on .minio.sys/ bucket
creation.

Allow these special buckets.
2022-03-14 11:25:24 -07:00
Klaus Post c07af89e48 select: Add ScanRange to CSV&JSON (#14546)
Implements https://docs.aws.amazon.com/AmazonS3/latest/API/API_SelectObjectContent.html#AmazonS3-SelectObjectContent-request-ScanRange

Fixes #14539
2022-03-14 09:48:36 -07:00
Harshavardhana 9c846106fa decouple service accounts from root credentials (#14534)
changing root credentials makes service accounts
in-operable, this PR changes the way sessionToken
is generated for service accounts.

It changes service account behavior to generate
sessionToken claims from its own secret instead
of using global root credential.

Existing credentials will be supported by
falling back to verify using root credential.

fixes #14530
2022-03-14 09:09:22 -07:00
Harshavardhana cf94d1f1f1 do not crash readXLMetaNoData - if the xl.meta has incorrect content (#14538)
```
tmp = buf[want:]
```

Would potentially crash when `buf` is truncated for some reason
and does not have the expected bytes, this is of course considered
not normal and is an odd situation. But we do not need to crash
here instead allow for errors to be returned and let callers handle
the errors.
2022-03-14 09:07:46 -07:00
Harshavardhana 6187440f35 update helm release v3.6.0 2022-03-13 15:44:21 -07:00
Minio Trusted 57b7c3494f Update yaml files to latest version RELEASE.2022-03-11T23-57-45Z 2022-03-13 08:47:27 +00:00
Harshavardhana dda18c28c5 Bump github.com/nats-io/nats-server/v2 from 2.7.2 to 2.7.4 2022-03-11 15:57:45 -08:00
Poorna f8d6eaaa96 fix: regression from range GET proxy on replicated buckets #14345 (#14532)
Fixes: #14531
2022-03-11 15:56:49 -08:00
Vijay Dharap 47d4fabb58 add filesystem group change policy for large minio deployments (#14528)
* add group change policy for large MinIO deployments
* Added Kubernetes version > 1.20 check for applying the proposed change
2022-03-11 14:21:58 -08:00
Minio Trusted 80039f60d5 Update yaml files to latest version RELEASE.2022-03-11T11-08-23Z 2022-03-11 11:47:17 +00:00
Harshavardhana 5a5e9b8a89 update console to v0.15.2 2022-03-11 03:08:23 -08:00
Aditya Manthramurthy b7ed3b77bd Indicate required fields in LDAP configuration correctly (#14526) 2022-03-10 19:03:38 -08:00
Poorna 75b925c326 Deprecate root disk for disk caching (#14527)
This PR modifies #14513 to issue a deprecation
warning rather than reject settings on startup.
2022-03-10 18:42:44 -08:00
Harshavardhana 91d419ee6c warn issues about large block I/O performance for Linux older than 4.0.0 (#14524)
This PR simply adds a warning message when it detects older kernel
versions and warn's them about potential performance issues on this
kernel.

The issue can be seen only with parallel I/O across all drives
on denser setups such as 90 drives or 45 drives per server configurations.
2022-03-10 17:36:13 -08:00
Harshavardhana 23345098ea change dperf to use standard Go io.Copy 2022-03-10 12:53:39 -08:00
Poorna 7ce91ea1a1 Disallow root disk to be used for cache drives (#14513) 2022-03-10 02:45:31 -08:00
Harshavardhana 41079f1015 heal: remove blocking healDiskMeta upon startup (#14514)
This type of code is not necessary, read's of all
metadata content at `.minio.sys/config` automatically
triggers healing when necessary in the GetObjectNInfo()
call-path.

Having this code is not useful and this also adds to
the overall startup time of MinIO when there are lots
of users and policies.
2022-03-10 02:45:14 -08:00
Poorna 712dfa40cd Add missing site replication hook for clearing sse config (#14512) 2022-03-10 00:04:34 -08:00
Harshavardhana decfd6108c update dperf to calculate timing for fdatasync()/close() calls as well 2022-03-09 13:47:44 -08:00
Klaus Post b890bbfa63 Add local disk health checks (#14447)
The main goal of this PR is to solve the situation where disks stop 
responding to operations. This generally causes an FD build-up and 
eventually will crash the server.

This adds detection of hung disks, where calls on disk get stuck.

We add functionality to `xlStorageDiskIDCheck` where it keeps 
track of the number of concurrent requests on a given disk.

A total number of 100 operations are allowed. If this limit is reached 
we will block (but not reject) new requests, but we will monitor the 
state of the disk.

If no requests have been completed or updated within a 15-second 
window, we mark the disk as offline. Requests that are blocked will be 
unblocked and return an error as "faulty disk".

New requests will be rejected until the disk is marked OK again.

Once a disk has been marked faulty, a check will run every 5 seconds that 
will attempt to write and read back a file. As long as this fails the disk will 
remain faulty.

To prevent lots of long-running requests to mark the disk faulty we 
implement a callback feature that allows updating the status as parts 
of these operations are running.

We add a reader and writer wrapper that will update the status of each 
successful read/write operation. This should allow fine enough granularity 
that a slow, but still operational disk will not reach 15 seconds where 
50 operations have not progressed.

Note that errors themselves are not enough to mark a disk faulty. 
A nil (or io.EOF) error will mark a disk as "good".

* Make concurrent disk setting configurable via `_MINIO_DISK_MAX_CONCURRENT`.

* de-couple IsOnline() from disk health tracker

The purpose of IsOnline() is to ensure that we
reconnect the drive only when the "drive" was

- disconnected from network we need to validate
  if the drive is "correct" and is the same drive
  which belongs to this server.

- drive was replaced we have to format it - we
  support hot swapping of the drives.

IsOnline() is not meant for taking the drive offline
when it is hung, it is not useful we can let the
drive be online instead "return" errors for relevant
calls.

* return errFaultyDisk for DiskInfo() call

Co-authored-by: Harshavardhana <harsha@minio.io>

Possible future Improvements:

* Unify the REST server and local xlStorageDiskIDCheck. This would also improve stats significantly.
* Allow reads/writes to be aborted by the context.
* Add usage stats, concurrent count, blocked operations, etc.
2022-03-09 11:38:54 -08:00
Daichi Mukai 0e3a570b85 helm: add namespace to StatefulSet (#14509)
Even if we specify the target namespace by `helm install --namespace`, 
the StatefulSet is created on the default namespace. Since this resource
references the ServiceAccount created on the target namespace, pods are
hindered to be created. To avoid this, we deploy the StatefulSet to the
target namespace of helm.
2022-03-09 11:25:36 -08:00
Klaus Post 7060c809c0 Add authorization header to HEAD requests (#14510)
Add Authorization to network check requests.

Fixes #14507
2022-03-09 10:48:56 -08:00
Andreas Auernhammer 9dbfd84c5b CI: use MINIO_KMS_SECRET_KEY when verify healing (#14511)
This commit replaces the KMS / KES environment
variables with `MINIO_KMS_SECRET_KEY` when testing
healing on CI.

This change is necessary since KES `0.18.0` introduced
some API breaking changes and the healing tests run
a test (`verify-3604`) that requires an older MinIO
version (e.g. `2021-11-24T23-19-33Z`) which is not
able to parse a KES error as expected.

This commit allows the KES instance at `https://play.min.io:7373`
to get updated to newer versions.

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-09 10:48:29 -08:00
Minio Trusted fce380a044 Update yaml files to latest version RELEASE.2022-03-08T22-28-51Z 2022-03-09 01:36:59 +00:00
Poorna 46ba15ab03 Return MethodNotAllowed if force del on replicated bucket (#14505) 2022-03-08 14:28:51 -08:00
Poorna 1e39ca39c3 fix: consistent replies for incorrect range requests on replicated buckets (#14345)
Propagate error from replication proxy target correctly to the client if range GET is unsatisfiable.
2022-03-08 13:58:55 -08:00
Krishnan Parthasarathi 80ef1ae51c Simplify assembling of tierStats from data-usage (#14504) 2022-03-08 12:08:29 -08:00
Krishna Srinivas 4d0715d226 Implement netperf for "mc support perf net" (#14397)
Co-authored-by: Klaus Post <klauspost@gmail.com>
2022-03-08 09:54:38 -08:00
Klaus Post 8a274169da heal: Fix first entry on dangling (#14495)
Instead of the first, the last entry was returned
pointerizing the range value.
2022-03-08 09:04:20 -08:00
Harshavardhana 21d8298fe1 update console UI to release v0.15.1 2022-03-07 23:40:58 -08:00
Harshavardhana 5d6f6d8d5b create missing .minio.sys/config, .minio.sys/buckets during decommission (#14497) 2022-03-07 16:18:57 -08:00
Anis Elleuch bacf6156c1 metrics: Avoid crash when fetching tier metrics (#14493)
Data usage does not always contain tiering info even if the data usage
information is valid. Avoid a crash in that case.

(e.g. the scanner scanned the namespace, the user enables tiering,
prometheus scrapes the server before the scanner gets a chance to
update the data usage with new tiering information)
2022-03-07 10:59:32 -08:00
Klaus Post 1d1b213f1f scanner: Consider preselection bias when selecting for Healing (#14492)
Healing decisions would align with skipped folder counters. This can lead to files 
never being selected for heal checks on "clean" paths.

Use different hashing methods and take objectHealProbDiv into account when 
calculating the cycle.

Found by @vadmeste
2022-03-07 09:25:53 -08:00
Minio Trusted 1f11af42f1 Update yaml files to latest version RELEASE.2022-03-05T06-32-39Z 2022-03-05 09:27:28 +00:00
Jan Madera a026c8748f Update nginx.conf for large file uploads (#14481) 2022-03-04 22:32:39 -08:00
David Young 9f7d89b3cd Add option to ignore checksumming config/secrets (#14396)
Signed-off-by: David Young <davidy@funkypenguin.co.nz>
2022-03-04 22:32:15 -08:00
Harshavardhana 92a77cc78e update pkg v1.1.20 to reload certs in k8s always (#14470) 2022-03-04 20:34:39 -08:00
Harshavardhana b0c84e3de7 fix: deleteVersions causing xl.meta to have empty Versions[] slice (#14483)
This is a side-affect of the optimization done in PR #13544 which
causes a certain type of delete operations on given object versions
can cause lastVersion indication to be skipped, which leads to
an `xl.meta` where Versions[] slice is empty while the entire
file is intact by itself.

This PR tries to ensure that such files are visible and deletable
by regular means of listing as null 'delete-marker' and also
avoid the situation where this potential issue might arise.
2022-03-04 20:01:26 -08:00
Anis Elleuch bbc914e174 heal: Do not override heal scan mode mode if it is set (#14476)
mc admin heal has --scan=deep flag which enforces bitrot checking 
when doing the healing.

Do not force override an existing heal scan option.
2022-03-04 18:25:06 -08:00
Anis Elleuch 3fca4055d2 heal: Re-heal an object when a corruption is found during normal scan (#14482)
When scanning using normal mode, HealObject() can report an 
error saying that it found a corrupted part. This doesn't have 
when HealObject() is called with bitrot scan flag. However, when 
this happens, we can still restart HealObject() with the bitrot scan.

This is also important because this means the scanner and the 
new disks healer will not be able to heal an object that doesn't 
exist in a specific disk and has corruption in another disk.

Also without this PR, mc admin heal command without bitrot will report
an error.
2022-03-04 18:24:34 -08:00
Harshavardhana 66afa16aed canceled PUTs throw frivolous logs (#14475)
remote drives might throw frivolous logs,
if the caller canceled the PUT operation
in such scenarios there is no reason to log.
2022-03-04 10:31:33 -08:00
Harshavardhana 9b0a8de7de update helm v3.5.9 2022-03-03 15:29:03 -08:00
Minio Trusted 04bbede17d Update yaml files to latest version RELEASE.2022-03-03T21-21-16Z 2022-03-03 22:16:10 +00:00
Harshavardhana 0e3bafcc54 improve logs, fix banner formatting (#14456) 2022-03-03 13:21:16 -08:00
Andreas Auernhammer b48f719b8e kes: remove unnecessary error conversion (#14459)
This commit removes some duplicate code that
converts KES API errors.

This code was added since KES `0.18.0` changed
some exported API errors. However, the KES SDK
handles this error conversion itself.
Therefore, it is not necessary to duplicate this
behavior in MinIO.

See: https://github.com/minio/kes/blob/21555fa624420def4aa4766686baa553b692010a/error.go#L94

Signed-off-by: Andreas Auernhammer <hi@aead.dev>
2022-03-03 09:42:37 -08:00
Lenin Alevski 289fcbd08c KES dependency upgrade (#14454)
- Updating KES dependency to v.0.18.0
- Fixing incompatibility issue when checking for errors during KES key creation

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2022-03-02 23:03:40 -08:00
Harshavardhana f6875bb893 fix: regression from refactor in AMQP notification (#14455)
fixes a regression introduced in #14269 that refactored
the notification registration logic, all the amqp targets
however online will not be available for use anymore.

fixes #14451
2022-03-02 21:35:48 -08:00
Harshavardhana 7e803adf13 do not attempt force delete on bucket (#14452)
caller needs to ask explicitly for force delete
otherwise, the force delete might end up deleting
an existing bucket with data.

fixes #14445
2022-03-02 20:47:53 -08:00
Harshavardhana 5b5deee5b3 update minio/pkg to v1.1.18 2022-03-02 19:25:07 -08:00
Krishnan Parthasarathi 7dae4cb685 Update minio/pkg to v1.1.17 (#14450)
Fix for admin policy validation of KMSCreateKey
2022-03-02 17:06:06 -08:00
Emmet McPoland 27fad98179 Replace HeadBucket permission with GetBucketAcl (#14436)
Resolves https://github.com/minio/minio/issues/14379
2022-03-01 21:18:23 -08:00
Harshavardhana 58f7e3a829 update console v0.15.0, coredns v1.9.0 2022-03-01 17:17:18 -08:00
Anis Elleuch 4a15bd8ff8 Return info for DiskInfo when the disk is unformatted (#14427)
In a distributed setup, a DiskInfo REST call to an unformatted disk
returns an error with no disk information, such as the disk endpoint
URL, which is unexpected.
2022-03-01 15:06:47 -08:00
Klaus Post b030ef1aca tests: Clean up dsync package (#14415)
Add non-constant timeouts to dsync package.

Reduce test runtime by minutes. Hopefully not too aggressive.
2022-03-01 11:14:28 -08:00
Harshavardhana cc46a99f97 skip object-lock headers without values (#14430)
metadata headers can have headers without values
as per AWS S3 spec however, we need to skip some
headers that do not have values that potentially
can have empty values set.
2022-03-01 11:04:47 -08:00
Xuehan Xu becec6cb6b correct mrf.newSetReconnected invocation's param order (#14426)
Signed-off-by: xuxuehan <xuxuehan@qianxin.com>
2022-02-28 09:13:19 -08:00
Harshavardhana bc33db9fc0 update helm v3.5.8 2022-02-26 22:44:38 -08:00
Minio Trusted 7d4579e737 Update yaml files to latest version RELEASE.2022-02-26T02-54-46Z 2022-02-26 03:36:08 +00:00
Harshavardhana b7c90751b0 allow drive tests to respond only drive paths 2022-02-25 18:54:46 -08:00
Klaus Post 88fd1cba71 select: add MISSING operator support (#14406)
Probably not full support, but for regular checks it should work.

Fixes #14358
2022-02-25 12:31:19 -08:00
Harshavardhana e43cc316ff remove errCh usage from HealObjects() simplify it (#14414)
errCh is not needed instead, rely on errs slice to
capture and return errors instead.

most probably fixes #14247
2022-02-25 12:20:41 -08:00
Klaus Post e3f24a29fa Upgrade simdjson & compress deps (#14411) 2022-02-25 10:48:41 -08:00
Harshavardhana 890e526bde rename 'mc admin inspect' to 'mc support inspect' 2022-02-24 17:17:53 -08:00
Harshavardhana 16ce455fca update docker release to RELEASE.2022-02-24T22-12-01Z 2022-02-24 15:35:14 -08:00
Harshavardhana 29b7164468 update console update v0.14.8 2022-02-24 14:12:01 -08:00
Harshavardhana acdd03f609 update CREDITs file for new dependencies 2022-02-24 12:58:53 -08:00
hellivan 03b35ecdd0 collect correct parentUser for OIDC creds auto expiration (#14400) 2022-02-24 11:43:15 -08:00
hellivan 5307e18085 use keycloak_realm properly for keycloak user lookups (#14401)
In case a user-defined a value for the MINIO_IDENTITY_OPENID_KEYCLOAK_REALM 
environment variable, construct the path properly.
2022-02-24 10:16:53 -08:00
Klaus Post 2cea944cdb select: Allow lower case 'is' (#14405)
Ref: #14358
2022-02-24 09:10:48 -08:00
Harshavardhana c08540c7b7 reject speedtest when there isn't enough disk space available (#14402)
small setups do not return appropriate errors when speedtest
cannot run on small tiny setups, allow the tests to fail
appropriately more pro-actively.

many users bring toy setups, this PR simply returns an error
in such situations.
2022-02-24 09:06:18 -08:00
Shireesh Anjal 3934700a08 Make audit webhook and kafka config dynamic (#14390) 2022-02-24 09:05:33 -08:00
hellivan 0913eb6655 fix: openid config provider not initialized correctly (#14399)
Up until now `InitializeProvider` method of `Config` struct was
implemented on a value receiver which is why changes on `provider`
field where never reflected to method callers. In order to fix this
issue, the method was implemented on a pointer receiver.
2022-02-23 23:42:37 -08:00
Harshavardhana 1bfbe354f5 fix: clientId must be unique for all servers (#14398)
This is a regression from #14037, distributed setups
with MQTT was not working anymore. According to MQTT
spec it is expected this is unique per server.

We shall proceed to use unix nano timestamp hex
value instead here.
2022-02-23 20:19:59 -08:00
Harshavardhana 2d78e20120 enable CI environment additionally for MINIO_CI_CD (#14395)
all CI/CD environments set CI=true this is enough
for MinIO to be run inside CI environments, support
it.
2022-02-23 16:01:59 -08:00
Harshavardhana 77210513c9 update minio/pkg, minio/madmin-go, minio/minio-go/v7 2022-02-23 14:34:47 -08:00
Harshavardhana 2e6f8bdf19 do not skip healing disks during deletes (#14394)
healing disks take active I/O it is possible
that deleted objects might stay in .trash
folder for a really long time until the drive
is fully healed.

this PR changes it such that we are making sure
we purge the active content written to these
disks as well.
2022-02-23 14:30:46 -08:00
Shireesh Anjal 25144fedd5 Send deployment id and minio version in http header (#14378) 2022-02-23 13:36:01 -08:00
Krishnan Parthasarathi 27f64dd9a4 Add support for tier-remove and tier-verify (#14382)
* Add tier remove support only if it's empty
* Add support for tier verify
2022-02-23 13:34:25 -08:00
Harshavardhana 9d7648f02f reduce unnecessary logging during speedtest (#14387)
- speedtest logs calls that were canceled
  spuriously, in situations where it should
  be ignored.

- all errors of interest are always sent back
  to the client there is no need to log them
  on the server console.

- PUT failures should negate the increments
  such that GET is not attempted on unsuccessful
  calls.

- do not attempt MRF on speedtest objects.
2022-02-23 11:59:13 -08:00
Poorna 1ef8babfef cache: improve error reported for atime check (#14384) 2022-02-23 11:57:06 -08:00
Poorna 4ea7bf0510 Use custom transport for site replication (#14391)
Also, ensure that tiering uses a different instance of custom transport
2022-02-23 11:50:40 -08:00
Anis Elleuch 5dcf1d13a9 ci: Always set disks as non root disks (#14389)
In the testing mode, reformatting disks will fail because the healing
code will complain if one disk is in root mode. This commit will
automatically set all disks as non-root if MINIO_CI_CD is set.
2022-02-23 10:11:33 -08:00
Shireesh Anjal 94d37d05e5 Apply dynamic config at sub-system level (#14369)
Currently, when applying any dynamic config, the system reloads and
re-applies the config of all the dynamic sub-systems.

This PR refactors the code in such a way that changing config of a given
dynamic sub-system will work on only that sub-system.
2022-02-22 10:59:28 -08:00
Harshavardhana 0cbdc458c5 fix: do not reload disk format.json on a reconnected disk (#14351)
An onlineDisk means its a valid disk but it may be a
re-connected disk, this PR verifies that based on LastConn()
to only trigger MRF. Current code would again re-load the
disk 'format.json' which is not necessary and perhaps an
unnecessary call.

A potential side affect of this is closing perfectly online
disks and getting re-replaced by reloading 'format.json'.

This PR tries to avoid this situation by making sure MRF
is triggered but not reloading 'format.json' because of MRF.
2022-02-21 15:51:54 -08:00
Shireesh Anjal c1437c7b46 allow config reset api to work by overloading default values (#14368)
The `LookupConfig` code was not using `GetWithDefault`, because of which
some of the config values were being returned as empty string, and calls
like `strconv.Atoi` and `time.ParseDuration` on these were failing.
2022-02-21 15:50:45 -08:00
Eric f357f65d04 Allow policy bootstrapping with nil "Resource" (#14359) 2022-02-20 15:56:41 -08:00
Harshavardhana ef8e952fc4 update helm v3.5.7 2022-02-20 00:55:08 -08:00
Eric a2bc383e15 Allow bootstrapping policies with special characters in Helm (#14356)
If the policy fails MinIO's minimum threshold for a valid policy,
they'll still (correctly) fail, but policies with a : (and probably a
/) should be allowed since they work with standard MC/MinIO 
Console interactions.

This creates the files as policy_IDX.json instead of <name>.json 
to avoid any issues with the name + Kubernetes ConfigMaps since 
ConfigMap keys must be: [-._a-zA-Z0-9]+
2022-02-19 23:21:17 -08:00
Harshavardhana 23930355a7 rename 'config host add' -> 'alias set'
update helm to v3.5.6
2022-02-19 12:34:14 -08:00
Domonkos Cinke bb9f41e613 Add ability to use custom commands (#14227) 2022-02-19 12:29:15 -08:00
Aditya Manthramurthy bc110d8055 fix: mysql notification target table creation (#14350)
Add a generated hash column as the primary key for the key name as 
MySQL does not allow indexes on long VARCHAR columns.
2022-02-18 12:13:49 -08:00
Minio Trusted b23b19e5c3 Update yaml files to latest version RELEASE.2022-02-18T01-50-10Z 2022-02-17 19:12:27 -08:00
187 changed files with 8177 additions and 7834 deletions
+6 -2
View File
@@ -11,6 +11,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
@@ -20,10 +23,11 @@ jobs:
go-version: [1.17.x]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Build on ${{ matrix.os }}
if: matrix.os == 'ubuntu-latest'
env:
+3 -5
View File
@@ -21,9 +21,10 @@ jobs:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- uses: actions/cache@v2
with:
path: |
@@ -37,10 +38,7 @@ jobs:
env:
CGO_ENABLED: 0
GO111MODULE: on
MINIO_KMS_KES_CERT_FILE: /home/runner/work/minio/minio/.github/workflows/root.cert
MINIO_KMS_KES_KEY_FILE: /home/runner/work/minio/minio/.github/workflows/root.key
MINIO_KMS_KES_ENDPOINT: "https://play.min.io:7373"
MINIO_KMS_KES_KEY_NAME: "my-minio-key"
MINIO_KMS_SECRET_KEY: "my-minio-key:oyArl7zlPECEduNbB1KXgdzDn2Bdpvvw0l8VO51HQnY="
MINIO_KMS_AUTO_ENCRYPTION: on
run: |
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
+2 -1
View File
@@ -21,9 +21,10 @@ jobs:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- uses: actions/cache@v2
if: matrix.os == 'ubuntu-latest'
with:
+2 -1
View File
@@ -21,9 +21,10 @@ jobs:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- uses: actions/cache@v2
with:
path: |
+2 -1
View File
@@ -65,9 +65,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- uses: actions/cache@v2
with:
path: |
+2 -1
View File
@@ -22,9 +22,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- uses: actions/cache@v2
with:
path: |
+2 -2
View File
@@ -22,10 +22,10 @@ jobs:
steps:
- uses: actions/checkout@v1
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Start upgrade tests
run: |
make test-upgrade
+1222 -5213
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -182,6 +182,12 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
Description: err.Error(),
HTTPStatusCode: http.StatusConflict,
}
case errors.Is(err, errTierBackendNotEmpty):
apiErr = APIError{
Code: "XMinioAdminTierBackendNotEmpty",
Description: err.Error(),
HTTPStatusCode: http.StatusBadRequest,
}
case errors.Is(err, errTierInsufficientCreds):
apiErr = APIError{
Code: "XMinioAdminTierInsufficientCreds",
+13 -6
View File
@@ -64,6 +64,12 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
return
}
subSys, _, _, err := config.GetSubSys(string(kvBytes))
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
cfg, err := readServerConfig(ctx, objectAPI)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
@@ -74,7 +80,7 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err = validateConfig(cfg, ""); err != nil {
if err = validateConfig(cfg, subSys); err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
return
}
@@ -84,15 +90,16 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
return
}
dynamic := config.SubSystemsDynamic.Contains(string(kvBytes))
dynamic := config.SubSystemsDynamic.Contains(subSys)
if dynamic {
applyDynamic(ctx, objectAPI, cfg, r, w)
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
}
}
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, r *http.Request, w http.ResponseWriter) {
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, subSys string,
r *http.Request, w http.ResponseWriter) {
// Apply dynamic values.
if err := applyDynamicConfig(GlobalContext, objectAPI, cfg); err != nil {
if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, cfg, subSys); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
@@ -162,7 +169,7 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
}
if dynamic {
applyDynamic(ctx, objectAPI, cfg, r, w)
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
}
writeSuccessResponseHeadersOnly(w)
}
+108 -21
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2015-2021 MinIO, Inc.
// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
@@ -18,11 +18,12 @@
package cmd
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/subtle"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"io"
@@ -935,6 +936,50 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
}
}
// NetperfHandler - perform mesh style network throughput test
func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "NetperfHandler")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
if objectAPI == nil {
return
}
if !globalIsDistErasure {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
nsLock := objectAPI.NewNSLock(minioMetaBucket, "netperf")
lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
if err != nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
return
}
defer nsLock.Unlock(lkctx.Cancel)
durationStr := r.Form.Get(peerRESTDuration)
duration, err := time.ParseDuration(durationStr)
if err != nil {
duration = globalNetPerfMinDuration
}
if duration < globalNetPerfMinDuration {
// We need sample size of minimum 10 secs.
duration = globalNetPerfMinDuration
}
duration = duration.Round(time.Second)
results := globalNotificationSys.Netperf(ctx, duration)
enc := json.NewEncoder(w)
if err := enc.Encode(madmin.NetperfResult{NodeResults: results}); err != nil {
return
}
}
// SpeedtestHandler - Deprecated. See ObjectSpeedtestHandler
func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
a.ObjectSpeedtestHandler(w, r)
@@ -984,6 +1029,31 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
duration = time.Second * 10
}
// ignores any errors here.
storageInfo, _ := objectAPI.StorageInfo(ctx)
capacityNeeded := uint64(concurrent * size)
capacity := uint64(GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo))
if capacity < capacityNeeded {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, AdminError{
Code: "XMinioSpeedtestInsufficientCapacity",
Message: fmt.Sprintf("not enough usable space available to perform speedtest - expected %s, got %s",
humanize.IBytes(capacityNeeded), humanize.IBytes(capacity)),
StatusCode: http.StatusInsufficientStorage,
}), r.URL)
return
}
// Verify if we can employ autotune without running out of capacity,
// if we do run out of capacity, make sure to turn-off autotuning
// in such situations.
newConcurrent := concurrent + (concurrent+1)/2
autoTunedCapacityNeeded := uint64(newConcurrent * size)
if autotune && capacity < autoTunedCapacityNeeded {
// Turn-off auto-tuning if next possible concurrency would reach beyond disk capacity.
autotune = false
}
deleteBucket := func() {
objectAPI.DeleteBucket(context.Background(), pathJoin(minioMetaBucket, "speedtest"), DeleteBucketOptions{
Force: true,
@@ -1615,7 +1685,10 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
logger.LogIf(ctx, enc.Encode(healthInfo))
}
deadline := 1 * time.Hour
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
if query.Get("perfnet") != "" || query.Get("perfdrive") != "" {
deadline = 1 * time.Hour
}
if dstr := r.Form.Get("deadline"); dstr != "" {
var err error
deadline, err = time.ParseDuration(dstr)
@@ -2222,7 +2295,7 @@ func fetchKMSStatus() madmin.KMS {
func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
var loggerInfo []madmin.Logger
var auditloggerInfo []madmin.Audit
for _, target := range logger.Targets() {
for _, target := range logger.SystemTargets() {
if target.Endpoint() != "" {
tgt := target.String()
err := checkConnection(target.Endpoint(), 15*time.Second)
@@ -2262,19 +2335,9 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
defer cancel()
client := &http.Client{Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: xhttp.NewCustomDialContext(timeout),
ResponseHeaderTimeout: 5 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 5 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: globalRootCAs},
// Go net/http automatically unzip if content-type is
// gzip disable this feature, as we are always interested
// in raw stream.
DisableCompression: true,
}}
defer client.CloseIdleConnections()
client := &http.Client{
Transport: globalProxyTransport,
}
req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpointStr, nil)
if err != nil {
@@ -2367,8 +2430,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
// of profiling data of all nodes
zipWriter := zip.NewWriter(encw)
defer zipWriter.Close()
err = o.GetRawData(ctx, volume, file, func(r io.Reader, host, disk, filename string, si StatInfo) error {
rawDataFn := func(r io.Reader, host, disk, filename string, si StatInfo) error {
// Prefix host+disk
filename = path.Join(host, disk, filename)
if si.Dir {
@@ -2401,8 +2463,33 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
logger.LogIf(ctx, err)
}
return nil
})
logger.LogIf(ctx, err)
}
err = o.GetRawData(ctx, volume, file, rawDataFn)
if !errors.Is(err, errFileNotFound) {
logger.LogIf(ctx, err)
}
// save the format.json as part of inspect by default
if volume != minioMetaBucket && file != formatConfigFile {
err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
}
if !errors.Is(err, errFileNotFound) {
logger.LogIf(ctx, err)
}
// save args passed to inspect command
inspectArgs := []string{fmt.Sprintf(" Inspect path: %s%s%s\n", volume, slashSeparator, file)}
cmdLine := []string{"Server command line args: "}
for _, pool := range globalEndpoints {
cmdLine = append(cmdLine, pool.CmdLine)
}
cmdLine = append(cmdLine, "\n")
inspectArgs = append(inspectArgs, cmdLine...)
inspectArgsBytes := []byte(strings.Join(inspectArgs, " "))
if err = rawDataFn(bytes.NewReader(inspectArgsBytes), "", "", "inspect-input.txt", StatInfo{
Size: int64(len(inspectArgsBytes)),
}); err != nil {
logger.LogIf(ctx, err)
}
}
func createHostAnonymizerForFSMode() map[string]string {
+2 -1
View File
@@ -700,8 +700,9 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
}
if source.opts != nil {
task.opts = *source.opts
} else {
task.opts.ScanMode = globalHealConfig.ScanMode()
}
task.opts.ScanMode = globalHealConfig.ScanMode()
h.mutex.Lock()
h.scannedItemsMap[healType]++
+3 -2
View File
@@ -191,7 +191,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveTierHandler)))
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.VerifyTierHandler)))
// Tier stats
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
@@ -223,7 +224,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedtestHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(httpTraceHdrs(adminAPI.ObjectSpeedtestHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(httpTraceHdrs(adminAPI.DriveSpeedtestHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(httpTraceHdrs(adminAPI.NetSpeedtestHandler))
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(httpTraceHdrs(adminAPI.NetperfHandler))
// HTTP Trace
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
+1 -1
View File
@@ -50,7 +50,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
}
_, present := network[nodeName]
if !present {
if err := isServerResolvable(endpoint, 2*time.Second); err == nil {
if err := isServerResolvable(endpoint, 5*time.Second); err == nil {
network[nodeName] = string(madmin.ItemOnline)
} else {
network[nodeName] = string(madmin.ItemOffline)
+5
View File
@@ -126,6 +126,11 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
// Set all other user defined metadata.
for k, v := range objInfo.UserDefined {
// Empty values for object lock and retention can be skipped.
if v == "" && equals(k, xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate) {
continue
}
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
// Do not need to send any internal metadata
// values to client.
+44 -17
View File
@@ -197,13 +197,7 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
return claims
}
// Fetch claims in the security token returned by the client.
func getClaimsFromToken(token string) (map[string]interface{}, error) {
if token == "" {
claims := xjwt.NewMapClaims()
return claims.Map(), nil
}
func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{}, error) {
// JWT token for x-amz-security-token is signed with admin
// secret key, temporary credentials become invalid if
// server admin credentials change. This is done to ensure
@@ -212,9 +206,15 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
// hijacking the policies. We need to make sure that this is
// based an admin credential such that token cannot be decoded
// on the client side and is treated like an opaque value.
claims, err := auth.ExtractClaims(token, globalActiveCred.SecretKey)
claims, err := auth.ExtractClaims(token, secret)
if err != nil {
return nil, errAuthentication
if subtle.ConstantTimeCompare([]byte(secret), []byte(globalActiveCred.SecretKey)) == 1 {
return nil, errAuthentication
}
claims, err = auth.ExtractClaims(token, globalActiveCred.SecretKey)
if err != nil {
return nil, errAuthentication
}
}
// If OPA is set, return without any further checks.
@@ -241,23 +241,50 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
return claims.Map(), nil
}
// Fetch claims in the security token returned by the client.
func getClaimsFromToken(token string) (map[string]interface{}, error) {
return getClaimsFromTokenWithSecret(token, globalActiveCred.SecretKey)
}
// Fetch claims in the security token returned by the client and validate the token.
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
token := getSessionToken(r)
if token != "" && cred.AccessKey == "" {
// x-amz-security-token is not allowed for anonymous access.
return nil, ErrNoAccessKey
}
if cred.IsServiceAccount() && token == "" {
token = cred.SessionToken
}
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
if token == "" && cred.IsTemp() {
// Temporary credentials should always have x-amz-security-token
return nil, ErrInvalidToken
}
claims, err := getClaimsFromToken(token)
if err != nil {
return nil, toAPIErrorCode(r.Context(), err)
if token != "" && !cred.IsTemp() {
// x-amz-security-token should not present for static credentials.
return nil, ErrInvalidToken
}
return claims, ErrNone
if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
// validate token for temporary credentials only.
return nil, ErrInvalidToken
}
secret := globalActiveCred.SecretKey
if cred.IsServiceAccount() {
token = cred.SessionToken
secret = cred.SecretKey
}
if token != "" {
claims, err := getClaimsFromTokenWithSecret(token, secret)
if err != nil {
return nil, toAPIErrorCode(r.Context(), err)
}
return claims, ErrNone
}
claims := xjwt.NewMapClaims()
return claims.Map(), ErrNone
}
// Check request auth type verifies the incoming http request
+14 -18
View File
@@ -263,7 +263,7 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...",
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content - use 'mc admin heal alias/ --verbose' to check the status",
drivesToHeal, defaultMonitorNewDiskInterval))
// Heal any disk format and metadata early, if possible.
@@ -277,14 +277,6 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
}
}
if err := bgSeq.healDiskMeta(objAPI); err != nil {
if newObjectLayerFn() != nil {
// log only in situations, when object layer
// has fully initialized.
logger.LogIf(bgSeq.ctx, err)
}
}
go monitorLocalDisksAndHeal(ctx, z, bgSeq)
}
@@ -333,7 +325,7 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
// Ensure that reformatting disks is finished
bgSeq.queueHealTask(healSource{bucket: nopHeal}, madmin.HealItemMetadata)
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal - 'mc admin heal alias/ --verbose' to check the status.",
len(healDisks)))
erasureSetInPoolDisksToHeal = make([]map[int][]StorageAPI, len(z.serverPools))
@@ -404,13 +396,15 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
go func(setIndex int, disks []StorageAPI) {
defer wg.Done()
for _, disk := range disks {
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
if serverDebugLog {
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
}
// So someone changed the drives underneath, healing tracker missing.
tracker, err := loadHealingTracker(ctx, disk)
if err != nil {
logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool",
disk, humanize.Ordinal(i+1))
logger.LogIf(ctx, fmt.Errorf("Healing tracker missing on '%s', disk was swapped again on %s pool: %w",
disk, humanize.Ordinal(i+1), err))
tracker = newHealingTracker(disk)
}
@@ -438,12 +432,14 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
continue
}
logger.Info("Healing disk '%s' on %s pool, %s set complete", disk,
humanize.Ordinal(i+1), humanize.Ordinal(setIndex+1))
logger.Info("Summary:\n")
tracker.printTo(os.Stdout)
if serverDebugLog {
logger.Info("Healing disk '%s' on %s pool, %s set complete", disk,
humanize.Ordinal(i+1), humanize.Ordinal(setIndex+1))
logger.Info("Summary:\n")
tracker.printTo(os.Stdout)
logger.Info("\n")
}
logger.LogIf(ctx, tracker.delete(ctx))
logger.Info("\n")
// Only upon success pop the healed disk.
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
+1 -1
View File
@@ -206,7 +206,7 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
for _, clnt := range clnts {
if err := clnt.Verify(ctx, srcCfg); err != nil {
if !isNetworkError(err) {
logger.Info(fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err).Error())
logger.LogIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err))
}
offlineEndpoints = append(offlineEndpoints, clnt.String())
continue
+10 -1
View File
@@ -206,6 +206,15 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
// Call site replication hook.
//
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
Type: madmin.SRBucketMetaTypeSSEConfig,
Bucket: bucket,
SSEConfig: nil,
}); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
writeSuccessNoContent(w)
}
+11
View File
@@ -1270,6 +1270,17 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return
}
rcfg, err := getReplicationConfig(ctx, bucket)
switch {
case err != nil:
if _, ok := err.(BucketReplicationConfigNotFound); !ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return
}
case rcfg.HasActiveRules("", true):
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return
}
}
}
+17 -21
View File
@@ -24,28 +24,12 @@ import (
"strings"
"github.com/gorilla/mux"
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
"github.com/minio/pkg/bucket/policy"
)
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
g := errgroup.WithNErrs(len(objects)).WithConcurrency(500)
for index := range objects {
index := index
g.Go(func() error {
size, err := objects[index].GetActualSize()
if err == nil {
objects[index].Size = size
}
objects[index].ETag = objects[index].GetActualETag(nil)
return nil
}, index)
}
g.Wait()
}
// Validate all the ListObjects query arguments, returns an APIErrorCode
// if one of the args do not meet the required conditions.
// Special conditions required by MinIO server are as below
@@ -116,7 +100,10 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
return
}
concurrentDecryptETag(ctx, listObjectVersionsInfo.Objects)
if err = DecryptETags(ctx, GlobalKMS, listObjectVersionsInfo.Objects, kms.BatchSize()); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
@@ -178,7 +165,10 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
return
}
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects, kms.BatchSize()); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
// The next continuation token has id@node_index format to optimize paginated listing
nextContinuationToken := listObjectsV2Info.NextContinuationToken
@@ -253,7 +243,10 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
return
}
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects, kms.BatchSize()); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
@@ -350,7 +343,10 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
return
}
concurrentDecryptETag(ctx, listObjectsInfo.Objects)
if err = DecryptETags(ctx, GlobalKMS, listObjectsInfo.Objects, kms.BatchSize()); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
+25
View File
@@ -20,6 +20,7 @@ package cmd
import (
"bytes"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
@@ -27,6 +28,7 @@ import (
"time"
"github.com/minio/minio/internal/bucket/replication"
xhttp "github.com/minio/minio/internal/http"
)
//go:generate msgp -file=$GOFILE
@@ -699,3 +701,26 @@ func newBucketResyncStatus(bucket string) BucketReplicationResyncStatus {
Version: resyncMetaVersion,
}
}
var contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`)
// parse size from content-range header
func parseSizeFromContentRange(h http.Header) (sz int64, err error) {
cr := h.Get(xhttp.ContentRange)
if cr == "" {
return sz, fmt.Errorf("Content-Range not set")
}
parts := contentRangeRegexp.FindStringSubmatch(cr)
if len(parts) != 4 {
return sz, fmt.Errorf("invalid Content-Range header %s", cr)
}
if parts[3] == "*" {
return -1, nil
}
var usz uint64
usz, err = strconv.ParseUint(parts[3], 10, 64)
if err != nil {
return sz, err
}
return int64(usz), nil
}
+51 -23
View File
@@ -425,7 +425,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
wg.Add(1)
go func(index int, tgt *TargetClient) {
defer wg.Done()
rinfo := replicateDeleteToTarget(ctx, dobj, objectAPI, tgt)
rinfo := replicateDeleteToTarget(ctx, dobj, tgt)
rinfos.Targets[index] = rinfo
}(idx, tgt)
}
@@ -482,7 +482,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
}
}
func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, tgt *TargetClient) (rinfo replicatedTargetInfo) {
versionID := dobj.DeleteMarkerVersionID
if versionID == "" {
versionID = dobj.VersionID
@@ -1531,16 +1531,21 @@ func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
go globalReplicationStats.loadInitialReplicationMetrics(ctx)
}
type proxyResult struct {
Proxy bool
Err error
}
// get Reader from replication target if active-active replication is in place and
// this node returns a 404
func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy bool) {
tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, opts, proxyTargets)
if !proxy {
return nil, false
func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy proxyResult, err error) {
tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
if !proxy.Proxy {
return nil, proxy, nil
}
fn, off, length, err := NewGetObjectReader(rs, oi, opts)
fn, _, _, err := NewGetObjectReader(nil, oi, opts)
if err != nil {
return nil, false
return nil, proxy, err
}
gopts := miniogo.GetObjectOptions{
VersionID: opts.VersionID,
@@ -1548,30 +1553,40 @@ func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs
Internal: miniogo.AdvancedGetOptions{
ReplicationProxyRequest: "true",
},
PartNumber: opts.PartNumber,
}
// get correct offsets for encrypted object
if off >= 0 && length >= 0 {
if err := gopts.SetRange(off, off+length-1); err != nil {
return nil, false
if rs != nil {
h, err := rs.ToHeader()
if err != nil {
return nil, proxy, err
}
gopts.Set(xhttp.Range, h)
}
// Make sure to match ETag when proxying.
if err = gopts.SetMatchETag(oi.ETag); err != nil {
return nil, false
return nil, proxy, err
}
c := miniogo.Core{Client: tgt.Client}
obj, _, _, err := c.GetObject(ctx, bucket, object, gopts)
obj, _, h, err := c.GetObject(ctx, bucket, object, gopts)
if err != nil {
return nil, false
return nil, proxy, err
}
closeReader := func() { obj.Close() }
reader, err := fn(obj, h, closeReader)
if err != nil {
return nil, false
return nil, proxy, err
}
reader.ObjInfo = oi.Clone()
return reader, true
if rs != nil {
contentSize, err := parseSizeFromContentRange(h)
if err != nil {
return nil, proxy, err
}
reader.ObjInfo.Size = contentSize
}
return reader, proxyResult{Proxy: true}, nil
}
func getproxyTargets(ctx context.Context, bucket, object string, opts ObjectOptions) (tgts *madmin.BucketTargets) {
@@ -1590,11 +1605,11 @@ func getproxyTargets(ctx context.Context, bucket, object string, opts ObjectOpti
return tgts
}
func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (tgt *TargetClient, oi ObjectInfo, proxy bool) {
func proxyHeadToRepTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (tgt *TargetClient, oi ObjectInfo, proxy proxyResult) {
// this option is set when active-active replication is in place between site A -> B,
// and site B does not have the object yet.
if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) { // true only when site B sets MinIOSourceProxyRequest header
return nil, oi, false
return nil, oi, proxy
}
for _, t := range proxyTargets.Targets {
tgt = globalBucketTargetSys.GetRemoteTargetClient(ctx, t.Arn)
@@ -1612,9 +1627,22 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts Objec
Internal: miniogo.AdvancedGetOptions{
ReplicationProxyRequest: "true",
},
PartNumber: opts.PartNumber,
}
if rs != nil {
h, err := rs.ToHeader()
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Invalid range header for %s/%s(%s) - %w", bucket, object, opts.VersionID, err))
continue
}
gopts.Set(xhttp.Range, h)
}
objInfo, err := tgt.StatObject(ctx, t.TargetBucket, object, gopts)
if err != nil {
if isErrInvalidRange(ErrorRespToObjectError(err, bucket, object)) {
return nil, oi, proxyResult{Err: err}
}
continue
}
@@ -1645,15 +1673,15 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts Objec
if ok {
oi.ContentEncoding = ce
}
return tgt, oi, true
return tgt, oi, proxyResult{Proxy: true}
}
return nil, oi, false
return nil, oi, proxy
}
// get object info from replication target if active-active replication is in place and
// this node returns a 404
func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (oi ObjectInfo, proxy bool) {
_, oi, proxy = proxyHeadToRepTarget(ctx, bucket, object, opts, proxyTargets)
func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (oi ObjectInfo, proxy proxyResult) {
_, oi, proxy = proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
return oi, proxy
}
+34 -13
View File
@@ -24,6 +24,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/gob"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
@@ -100,12 +101,16 @@ func init() {
PersistOnFailure: false,
}
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
containers := IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile()
// Call to refresh will refresh names in cache. If you pass true, it will also
// remove cached names not looked up since the last call to Refresh. It is a good idea
// to call this method on a regular interval.
go func() {
var t *time.Ticker
if IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile() {
if containers {
t = time.NewTicker(1 * time.Minute)
} else {
t = time.NewTicker(10 * time.Minute)
@@ -327,7 +332,7 @@ func checkUpdate(mode string) {
return
}
logStartupMessage(prepareUpdateMessage("\nRun `mc admin update`", lrTime.Sub(crTime)))
logger.Info(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
}
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
@@ -664,8 +669,6 @@ func handleCommonEnvVars() {
globalRootDiskThreshold = size
}
globalIsCICD = env.Get("MINIO_CI_CD", "") != ""
domains := env.Get(config.EnvDomain, "")
if len(domains) != 0 {
for _, domainName := range strings.Split(domains, config.ValueSeparator) {
@@ -762,7 +765,7 @@ func handleCommonEnvVars() {
" Please use %s and %s",
config.EnvAccessKey, config.EnvSecretKey,
config.EnvRootUser, config.EnvRootPassword)
logStartupMessage(color.RedBold(msg))
logger.Info(color.RedBold(msg))
}
globalActiveCred = cred
}
@@ -796,7 +799,29 @@ func handleCommonEnvVars() {
endpoints = append(endpoints, strings.Join(lbls, ""))
}
}
certificate, err := tls.LoadX509KeyPair(env.Get(config.EnvKESClientCert, ""), env.Get(config.EnvKESClientKey, ""))
// Manually load the certificate and private key into memory.
// We need to check whether the private key is encrypted, and
// if so, decrypt it using the user-provided password.
certBytes, err := os.ReadFile(env.Get(config.EnvKESClientCert, ""))
if err != nil {
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
}
keyBytes, err := os.ReadFile(env.Get(config.EnvKESClientKey, ""))
if err != nil {
logger.Fatal(err, "Unable to load KES client private key as specified by the shell environment")
}
privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes))
if len(rest) != 0 {
logger.Fatal(errors.New("private key contains additional data"), "Unable to load KES client private key as specified by the shell environment")
}
if x509.IsEncryptedPEMBlock(privateKeyPEM) {
keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(config.EnvKESClientPassword, "")))
if err != nil {
logger.Fatal(err, "Unable to decrypt KES client private key as specified by the shell environment")
}
keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes})
}
certificate, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
}
@@ -827,13 +852,6 @@ func handleCommonEnvVars() {
}
}
func logStartupMessage(msg string) {
if globalConsoleSys != nil {
globalConsoleSys.Send(msg, string(logger.All))
}
logger.StartupMessage(msg)
}
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
return nil, nil, false, nil
@@ -910,6 +928,9 @@ func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secu
}
secureConn = true
// Certs might be symlinks, reload them every 10 seconds.
manager.UpdateReloadDuration(10 * time.Second)
// syscall.SIGHUP to reload the certs.
manager.ReloadOnSignal(syscall.SIGHUP)
+103 -111
View File
@@ -44,8 +44,6 @@ import (
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/kms"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/http"
"github.com/minio/minio/internal/logger/target/kafka"
"github.com/minio/pkg/env"
)
@@ -353,12 +351,6 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
}
}
if config.LoggerSubSystems.Contains(subSys) {
if err := logger.ValidateSubSysConfig(s, subSys); err != nil {
return err
}
}
if config.NotifySubSystems.Contains(subSys) {
if err := notify.TestSubSysNotificationTargets(GlobalContext, s, NewGatewayHTTPTransport(), globalNotificationSys.ConfiguredTargetIDs(), subSys); err != nil {
return err
@@ -537,7 +529,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
}
if globalSTSTLSConfig.InsecureSkipVerify {
logger.Info("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify)
logger.LogIf(ctx, fmt.Errorf("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify))
}
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
@@ -566,36 +558,6 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
}
// Load logger targets based on user's configuration
loggerUserAgent := getUserAgent(getMinioMode())
loggerCfg, err := logger.LookupConfig(s)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize logger/audit targets: %w", err))
}
for _, l := range loggerCfg.AuditWebhook {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
l.UserAgent = loggerUserAgent
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
// Enable http audit logging
if err = logger.AddAuditTarget(http.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize server audit HTTP target: %w", err))
}
}
}
for _, l := range loggerCfg.AuditKafka {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
// Enable Kafka audit logging
if err = logger.AddAuditTarget(kafka.New(l)); err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize server audit Kafka target: %w", err))
}
}
}
globalConfigTargetList, err = notify.GetNotificationTargets(GlobalContext, s, NewGatewayHTTPTransport(), false)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
@@ -616,84 +578,114 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
}
}
// applyDynamicConfig will apply dynamic config values.
// Dynamic systems should be in config.SubSystemsDynamic as well.
func applyDynamicConfig(ctx context.Context, objAPI ObjectLayer, s config.Config) error {
// Read all dynamic configs.
// API
apiConfig, err := api.LookupConfig(s[config.APISubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Invalid api configuration: %w", err))
}
func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s config.Config, subSys string) error {
switch subSys {
case config.APISubSys:
apiConfig, err := api.LookupConfig(s[config.APISubSys][config.Default])
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Invalid api configuration: %w", err))
}
var setDriveCounts []int
if objAPI != nil {
setDriveCounts = objAPI.SetDriveCounts()
}
globalAPIConfig.init(apiConfig, setDriveCounts)
case config.CompressionSubSys:
cmpCfg, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default])
if err != nil {
return fmt.Errorf("Unable to setup Compression: %w", err)
}
// Validate if the object layer supports compression.
if objAPI != nil {
if cmpCfg.Enabled && !objAPI.IsCompressionSupported() {
return fmt.Errorf("Backend does not support compression")
}
}
globalCompressConfigMu.Lock()
globalCompressConfig = cmpCfg
globalCompressConfigMu.Unlock()
case config.HealSubSys:
healCfg, err := heal.LookupConfig(s[config.HealSubSys][config.Default])
if err != nil {
return fmt.Errorf("Unable to apply heal config: %w", err)
}
globalHealConfig.Update(healCfg)
case config.ScannerSubSys:
scannerCfg, err := scanner.LookupConfig(s[config.ScannerSubSys][config.Default])
if err != nil {
return fmt.Errorf("Unable to apply scanner config: %w", err)
}
// update dynamic scanner values.
scannerCycle.Update(scannerCfg.Cycle)
logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
case config.LoggerWebhookSubSys:
loggerCfg, err := logger.LookupConfigForSubSys(s, config.LoggerWebhookSubSys)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to load logger webhook config: %w", err))
}
userAgent := getUserAgent(getMinioMode())
for n, l := range loggerCfg.HTTP {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
l.UserAgent = userAgent
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
loggerCfg.HTTP[n] = l
}
}
err = logger.UpdateSystemTargets(loggerCfg)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %w", err))
}
case config.AuditWebhookSubSys:
loggerCfg, err := logger.LookupConfigForSubSys(s, config.AuditWebhookSubSys)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to load audit webhook config: %w", err))
}
userAgent := getUserAgent(getMinioMode())
for n, l := range loggerCfg.AuditWebhook {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
l.UserAgent = userAgent
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
loggerCfg.AuditWebhook[n] = l
}
}
// Compression
cmpCfg, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default])
if err != nil {
return fmt.Errorf("Unable to setup Compression: %w", err)
}
// Validate if the object layer supports compression.
if objAPI != nil {
if cmpCfg.Enabled && !objAPI.IsCompressionSupported() {
return fmt.Errorf("Backend does not support compression")
err = logger.UpdateAuditWebhookTargets(loggerCfg)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %w", err))
}
case config.AuditKafkaSubSys:
loggerCfg, err := logger.LookupConfigForSubSys(s, config.AuditKafkaSubSys)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to load audit kafka config: %w", err))
}
for n, l := range loggerCfg.AuditKafka {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
loggerCfg.AuditKafka[n] = l
}
}
err = logger.UpdateAuditKafkaTargets(loggerCfg)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %w", err))
}
}
// Heal
healCfg, err := heal.LookupConfig(s[config.HealSubSys][config.Default])
if err != nil {
return fmt.Errorf("Unable to apply heal config: %w", err)
}
// Scanner
scannerCfg, err := scanner.LookupConfig(s[config.ScannerSubSys][config.Default])
if err != nil {
return fmt.Errorf("Unable to apply scanner config: %w", err)
}
// Logger webhook
loggerCfg, err := logger.LookupConfig(s)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to load logger webhook config: %w", err))
}
userAgent := getUserAgent(getMinioMode())
for n, l := range loggerCfg.HTTP {
if l.Enabled {
l.LogOnce = logger.LogOnceIf
l.UserAgent = userAgent
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
loggerCfg.HTTP[n] = l
}
}
err = logger.UpdateTargets(loggerCfg)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %w", err))
}
// Apply configurations.
// We should not fail after this.
var setDriveCounts []int
if objAPI != nil {
setDriveCounts = objAPI.SetDriveCounts()
}
globalAPIConfig.init(apiConfig, setDriveCounts)
globalCompressConfigMu.Lock()
globalCompressConfig = cmpCfg
globalCompressConfigMu.Unlock()
globalHealConfig.Update(healCfg)
// update dynamic scanner values.
scannerCycle.Update(scannerCfg.Cycle)
logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
// Update all dynamic config values in memory.
globalServerConfigMu.Lock()
defer globalServerConfigMu.Unlock()
if globalServerConfig != nil {
for k := range config.SubSystemsDynamic {
globalServerConfig[k] = s[k]
globalServerConfig[subSys] = s[subSys]
}
return nil
}
// applyDynamicConfig will apply dynamic config values.
// Dynamic systems should be in config.SubSystemsDynamic as well.
func applyDynamicConfig(ctx context.Context, objAPI ObjectLayer, s config.Config) error {
for subSys := range config.SubSystemsDynamic {
err := applyDynamicConfigForSubSys(ctx, objAPI, s, subSys)
if err != nil {
return err
}
}
return nil
+2 -2
View File
@@ -74,7 +74,7 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
if err != nil {
return err
}
logger.Info("Attempting to re-encrypt IAM users and policies on etcd with %q (%s)", stat.DefaultKey, stat.Name)
logger.Info(fmt.Sprintf("Attempting to re-encrypt IAM users and policies on etcd with %q (%s)", stat.DefaultKey, stat.Name))
}
listCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
@@ -143,7 +143,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, encrypted bool) error {
if err != nil {
return err
}
logger.Info("Attempting to re-encrypt config, IAM users and policies on MinIO with %q (%s)", stat.DefaultKey, stat.Name)
logger.Info(fmt.Sprintf("Attempting to re-encrypt config, IAM users and policies on MinIO with %q (%s)", stat.DefaultKey, stat.Name))
}
var marker string
+7 -1
View File
@@ -25,6 +25,7 @@ import (
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/message/log"
"github.com/minio/minio/internal/logger/target/console"
"github.com/minio/minio/internal/logger/target/types"
"github.com/minio/minio/internal/pubsub"
xnet "github.com/minio/pkg/net"
)
@@ -77,7 +78,7 @@ func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan struct{}, node string, last int, logKind string, filter func(entry interface{}) bool) {
// Enable console logging for remote client.
if !sys.HasLogListeners() {
logger.AddTarget(sys)
logger.AddSystemTarget(sys)
}
cnt := 0
@@ -154,6 +155,11 @@ func (sys *HTTPConsoleLoggerSys) Content() (logs []log.Entry) {
func (sys *HTTPConsoleLoggerSys) Cancel() {
}
// Type - returns type of the target
func (sys *HTTPConsoleLoggerSys) Type() types.TargetType {
return types.TargetConsole
}
// Send log message 'e' to console and publish to console
// log pubsub system
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
+4 -4
View File
@@ -403,7 +403,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
if filter != nil && ok && existing.Compacted {
// If folder isn't in filter and we have data, skip it completely.
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
if f.healObjectSelect == 0 || !thisHash.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) {
if f.healObjectSelect == 0 || !thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healFolderInclude/folder.objectHealProbDiv) {
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
f.updateCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
if f.dataUsageScannerDebug {
@@ -482,7 +482,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
debug: f.dataUsageScannerDebug,
lifeCycle: activeLifeCycle,
replication: replicationCfg,
heal: thisHash.mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure,
heal: thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure,
}
// if the drive belongs to an erasure set
// that is already being healed, skip the
@@ -609,13 +609,13 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
// and the entry itself is compacted.
if !into.Compacted && f.oldCache.isCompacted(h) {
if !h.mod(f.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
if f.healObjectSelect == 0 || !h.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) {
if f.healObjectSelect == 0 || !h.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healFolderInclude/folder.objectHealProbDiv) {
// Transfer and add as child...
f.newCache.copyWithChildren(&f.oldCache, h, folder.parent)
into.addChild(h)
continue
}
folder.objectHealProbDiv = dataUsageUpdateDirCycles
folder.objectHealProbDiv = f.healFolderInclude
}
}
scanFolder(folder)
+6 -1
View File
@@ -31,6 +31,7 @@ import (
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/message/log"
"github.com/minio/minio/internal/logger/target/types"
)
type testLoggerI interface {
@@ -58,6 +59,10 @@ func (t *testingLogger) Init() error {
func (t *testingLogger) Cancel() {
}
func (t *testingLogger) Type() types.TargetType {
return types.TargetHTTP
}
func (t *testingLogger) Send(entry interface{}, errKind string) error {
t.mu.Lock()
defer t.mu.Unlock()
@@ -76,7 +81,7 @@ func (t *testingLogger) Send(entry interface{}, errKind string) error {
func addTestingLogging(t testLoggerI) func() {
tl := &testingLogger{t: t}
logger.AddTarget(tl)
logger.AddSystemTarget(tl)
return func() {
tl.mu.Lock()
defer tl.mu.Unlock()
+11
View File
@@ -369,6 +369,17 @@ func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
return uint32(xxhash.Sum64String(string(h)))%cycles == cycle%cycles
}
// modAlt returns true if the hash mod cycles == cycle.
// This is out of sync with mod.
// If cycles is 0 false is always returned.
// If cycles is 1 true is always returned (as expected).
func (h dataUsageHash) modAlt(cycle uint32, cycles uint32) bool {
if cycles <= 1 {
return cycles == 1
}
return uint32(xxhash.Sum64String(string(h))>>32)%(cycles) == cycle%cycles
}
// addChild will add a child based on its hash.
// If it already exists it will not be added again.
func (e *dataUsageEntry) addChild(hash dataUsageHash) {
+51 -18
View File
@@ -92,32 +92,37 @@ type DataUsageInfo struct {
}
func (dui DataUsageInfo) tierStats() []madmin.TierInfo {
if globalTierConfigMgr.Empty() {
if dui.TierStats == nil {
return nil
}
ts := make(map[string]madmin.TierStats)
// Add configured remote tiers
for tier := range globalTierConfigMgr.Tiers {
ts[tier] = madmin.TierStats{}
cfgs := globalTierConfigMgr.ListTiers()
if len(cfgs) == 0 {
return nil
}
ts := make(map[string]madmin.TierStats, len(cfgs)+1)
infos := make([]madmin.TierInfo, 0, len(ts))
// Add STANDARD (hot-tier)
ts[minioHotTier] = madmin.TierStats{}
infos = append(infos, madmin.TierInfo{
Name: minioHotTier,
Type: "internal",
})
// Add configured remote tiers
for _, cfg := range cfgs {
ts[cfg.Name] = madmin.TierStats{}
infos = append(infos, madmin.TierInfo{
Name: cfg.Name,
Type: cfg.Type.String(),
})
}
ts = dui.TierStats.adminStats(ts)
infos := make([]madmin.TierInfo, 0, len(ts))
for tier, st := range ts {
var tierType string
if tier == minioHotTier {
tierType = "internal"
} else {
tierType = globalTierConfigMgr.Tiers[tier].Type.String()
}
infos = append(infos, madmin.TierInfo{
Name: tier,
Type: tierType,
Stats: st,
})
for i := range infos {
info := infos[i]
infos[i].Stats = ts[info.Name]
}
sort.Slice(infos, func(i, j int) bool {
@@ -131,3 +136,31 @@ func (dui DataUsageInfo) tierStats() []madmin.TierInfo {
})
return infos
}
func (dui DataUsageInfo) tierMetrics() (metrics []Metric) {
if dui.TierStats == nil {
return nil
}
// e.g minio_cluster_ilm_transitioned_bytes{tier="S3TIER-1"}=136314880
// minio_cluster_ilm_transitioned_objects{tier="S3TIER-1"}=1
// minio_cluster_ilm_transitioned_versions{tier="S3TIER-1"}=3
for tier, st := range dui.TierStats.Tiers {
metrics = append(metrics, Metric{
Description: getClusterTransitionedBytesMD(),
Value: float64(st.TotalSize),
VariableLabels: map[string]string{"tier": tier},
})
metrics = append(metrics, Metric{
Description: getClusterTransitionedObjectsMD(),
Value: float64(st.NumObjects),
VariableLabels: map[string]string{"tier": tier},
})
metrics = append(metrics, Metric{
Description: getClusterTransitionedVersionsMD(),
Value: float64(st.NumVersions),
VariableLabels: map[string]string{"tier": tier},
})
}
return metrics
}
+5 -3
View File
@@ -24,6 +24,7 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -252,16 +253,17 @@ func (c *diskCache) diskUsageLow() bool {
// Returns if the disk usage reaches or exceeds configured cache quota when size is added.
// If current usage without size exceeds high watermark a GC is automatically queued.
func (c *diskCache) diskSpaceAvailable(size int64) bool {
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
gcTriggerPct := c.quotaPct * c.highWatermark / 100
di, err := disk.GetInfo(c.dir)
if err != nil {
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
logger.LogIf(ctx, err)
return false
}
if di.Total == 0 {
logger.Info("diskCache: Received 0 total disk size")
logger.LogIf(ctx, errors.New("diskCache: Received 0 total disk size"))
return false
}
usedPercent := float64(di.Used) * 100 / float64(di.Total)
+18 -4
View File
@@ -19,7 +19,6 @@ package cmd
import (
"context"
"errors"
"fmt"
"io"
"net/http"
@@ -32,6 +31,7 @@ import (
objectlock "github.com/minio/minio/internal/bucket/object/lock"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/config/cache"
"github.com/minio/minio/internal/disk"
"github.com/minio/minio/internal/hash"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
@@ -595,14 +595,25 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
if err != nil {
return nil, false, err
}
var warningMsg string
for i, dir := range config.Drives {
// skip diskCache creation for cache drives missing a format.json
if formats[i] == nil {
caches = append(caches, nil)
continue
}
if !globalIsCICD && len(warningMsg) == 0 {
rootDsk, err := disk.IsRootDisk(dir, "/")
if err != nil {
warningMsg = fmt.Sprintf("Invalid cache dir %s err : %s", dir, err.Error())
}
if rootDsk {
warningMsg = fmt.Sprintf("cache dir cannot be part of root disk: %s", dir)
}
}
if err := checkAtimeSupport(dir); err != nil {
return nil, false, errors.New("Atime support required for disk caching")
return nil, false, fmt.Errorf("Atime support required for disk caching, atime check failed with %w", err)
}
cache, err := newDiskCache(ctx, dir, config)
@@ -611,11 +622,14 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
}
caches = append(caches, cache)
}
if warningMsg != "" {
logger.Info(color.Yellow(fmt.Sprintf("WARNING: Usage of root disk for disk caching is deprecated: %s", warningMsg)))
}
return caches, migrating, nil
}
func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
logStartupMessage(color.Blue("Cache migration initiated ...."))
logger.Info(color.Blue("Cache migration initiated ...."))
g := errgroup.WithNErrs(len(c.cache))
for index, dc := range c.cache {
@@ -644,7 +658,7 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
// update migration status
c.migrating = false
logStartupMessage(color.Blue("Cache migration completed successfully."))
logger.Info(color.Blue("Cache migration completed successfully."))
}
// PutObject - caches the uploaded object for single Put operations
+119 -5
View File
@@ -19,6 +19,7 @@ package cmd
import (
"bufio"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
@@ -35,6 +36,7 @@ import (
"github.com/minio/kes"
"github.com/minio/minio/internal/crypto"
"github.com/minio/minio/internal/etag"
"github.com/minio/minio/internal/fips"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/kms"
@@ -71,18 +73,130 @@ const (
)
// KMSKeyID returns in AWS compatible KMS KeyID() format.
func (o ObjectInfo) KMSKeyID() string {
if len(o.UserDefined) == 0 {
func (o *ObjectInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
// KMSKeyID returns in AWS compatible KMS KeyID() format.
func (o *MultipartInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
// kmsKeyIDFromMetadata returns any AWS S3 KMS key ID in the
// metadata, if any. It returns an empty ID if no key ID is
// present.
func kmsKeyIDFromMetadata(metadata map[string]string) string {
const ARNPrefix = "arn:aws:kms:"
if len(metadata) == 0 {
return ""
}
kmsID, ok := o.UserDefined[crypto.MetaKeyID]
kmsID, ok := metadata[crypto.MetaKeyID]
if !ok {
return ""
}
if strings.HasPrefix(kmsID, "arn:aws:kms:") {
if strings.HasPrefix(kmsID, ARNPrefix) {
return kmsID
}
return "arn:aws:kms:" + kmsID
return ARNPrefix + kmsID
}
// DecryptETags dectypts all ObjectInfo ETags, if encrypted, using the KMS.
func DecryptETags(ctx context.Context, KMS kms.KMS, objects []ObjectInfo, batchSize int) error {
var (
metadata = make([]map[string]string, 0, batchSize)
buckets = make([]string, 0, batchSize)
names = make([]string, 0, batchSize)
)
for len(objects) > 0 {
var N int
if len(objects) < batchSize {
N = len(objects)
} else {
N = batchSize
}
// We have to conntect the KMS only if there is at least
// one SSE-S3 single-part object. SSE-C and SSE-KMS objects
// don't return the plaintext MD5 ETag and the ETag of
// SSE-S3 multipart objects is not encrypted.
// Therefore, we can skip the expensive KMS calls whenever
// there is no single-part SSE-S3 object entirely.
var containsSSES3SinglePart bool
for _, object := range objects[:N] {
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
containsSSES3SinglePart = true
break
}
}
if !containsSSES3SinglePart {
for i := range objects[:N] {
size, err := objects[i].GetActualSize()
if err != nil {
return err
}
objects[i].Size = size
objects[i].ETag = objects[i].GetActualETag(nil)
}
objects = objects[N:]
continue
}
// Now, there are some SSE-S3 single-part objects.
// We only request the decryption keys for them.
// We don't want to get the decryption keys for multipart
// or non-SSE-S3 objects.
//
// Therefore, we keep a map of indicies to remember which
// object was an SSE-S3 single-part object.
// Then we request the decryption keys for these objects.
// Finally, we decrypt the ETags of these objects using
// the decryption keys.
// However, we must also adjust the size and ETags of all
// objects (not just the SSE-S3 single part objects).
// For example, the ETag of SSE-KMS objects are random values
// and the size of an SSE-KMS object must be adjusted as well.
SSES3Objects := make(map[int]bool, 10)
metadata = metadata[:0:N]
buckets = buckets[:0:N]
names = names[:0:N]
for i, object := range objects[:N] {
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
metadata = append(metadata, object.UserDefined)
buckets = append(buckets, object.Bucket)
names = append(names, object.Name)
SSES3Objects[i] = true
}
}
keys, err := crypto.S3.UnsealObjectKeys(KMS, metadata, buckets, names)
if err != nil {
return err
}
var keyIndex int
for i := range objects[:N] {
size, err := objects[i].GetActualSize()
if err != nil {
return err
}
objects[i].Size = size
if !SSES3Objects[i] {
objects[i].ETag = objects[i].GetActualETag(nil)
} else {
ETag, err := etag.Parse(objects[i].ETag)
if err != nil {
return err
}
if ETag.IsEncrypted() {
tag, err := keys[keyIndex].UnsealETag(ETag)
if err != nil {
return err
}
ETag = etag.ETag(tag)
objects[i].ETag = ETag.String()
}
keyIndex++
}
}
objects = objects[N:]
}
return nil
}
// isMultipart returns true if the current object is
+4 -2
View File
@@ -39,8 +39,10 @@ func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket stri
defer NSUpdated(bucket, slashSeparator)
// Verify if bucket is valid.
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
return BucketNameInvalid{Bucket: bucket}
if !isMinioMetaBucketName(bucket) {
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
return BucketNameInvalid{Bucket: bucket}
}
}
storageDisks := er.getDisks()
+1 -3
View File
@@ -59,9 +59,7 @@ func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
// Based on the random shuffling return back randomized disks.
for _, i := range hashOrder(UTCNow().String(), len(disks)) {
if disks[i-1] != nil && disks[i-1].IsLocal() {
if disks[i-1].Healing() == nil && disks[i-1].IsOnline() {
newDisks = append(newDisks, disks[i-1])
}
newDisks = append(newDisks, disks[i-1])
}
}
return newDisks
+4 -1
View File
@@ -83,7 +83,10 @@ func commonTime(modTimes []time.Time) (modTime time.Time) {
}
// Beginning of unix time is treated as sentinel value here.
var timeSentinel = time.Unix(0, 0).UTC()
var (
timeSentinel = time.Unix(0, 0).UTC()
timeSentinel1970 = time.Unix(0, 1).UTC() // 1970 used for special cases when xlmeta.version == 0
)
// Boot modTimes up to disk count, setting the value to time sentinel.
func bootModtimes(diskCount int) []time.Time {
+8 -1
View File
@@ -931,5 +931,12 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
}
// Heal the object.
return er.healObject(healCtx, bucket, object, versionID, opts)
hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
if errors.Is(err, errFileCorrupt) && opts.ScanMode != madmin.HealDeepScan {
// Instead of returning an error when a bitrot error is detected
// during a normal heal scan, heal again with bitrot flag enabled.
opts.ScanMode = madmin.HealDeepScan
hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
}
return hr, err
}
+203 -58
View File
@@ -382,6 +382,13 @@ func TestHealingDanglingObject(t *testing.T) {
copy(disks, newDisks)
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
}
getDisk := func(n int) StorageAPI {
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock()
disk := disks[n]
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
return disk
}
// Remove 4 disks.
setDisks(nil, nil, nil, nil)
@@ -440,8 +447,8 @@ func TestHealingDanglingObject(t *testing.T) {
}
setDisks(orgDisks[:4]...)
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
disk := getDisk(0)
fileInfoPreHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
if err != nil {
t.Fatal(err)
}
@@ -458,7 +465,8 @@ func TestHealingDanglingObject(t *testing.T) {
t.Fatal(err)
}
fileInfoPostHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
disk = getDisk(0)
fileInfoPostHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
if err != nil {
t.Fatal(err)
}
@@ -488,7 +496,8 @@ func TestHealingDanglingObject(t *testing.T) {
setDisks(orgDisks[:4]...)
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
disk = getDisk(0)
fileInfoPreHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
if err != nil {
t.Fatal(err)
}
@@ -505,7 +514,8 @@ func TestHealingDanglingObject(t *testing.T) {
t.Fatal(err)
}
fileInfoPostHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
disk = getDisk(0)
fileInfoPostHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
if err != nil {
t.Fatal(err)
}
@@ -807,7 +817,140 @@ func TestHealObjectCorruptedPools(t *testing.T) {
}
}
func TestHealObjectCorrupted(t *testing.T) {
func TestHealObjectCorruptedXLMeta(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resetGlobalHealState()
defer resetGlobalHealState()
nDisks := 16
fsDirs, err := getRandomDisks(nDisks)
if err != nil {
t.Fatal(err)
}
defer removeRoots(fsDirs)
// Everything is fine, should return nil
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
if err != nil {
t.Fatal(err)
}
bucket := getRandomBucketName()
object := getRandomObjectName()
data := bytes.Repeat([]byte("a"), 5*1024*1024)
var opts ObjectOptions
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
if err != nil {
t.Fatalf("Failed to make a bucket - %v", err)
}
// Create an object with multiple parts uploaded in decreasing
// part number.
uploadID, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts)
if err != nil {
t.Fatalf("Failed to create a multipart upload - %v", err)
}
var uploadedParts []CompletePart
for _, partID := range []int{2, 1} {
pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, uploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
if err1 != nil {
t.Fatalf("Failed to upload a part - %v", err1)
}
uploadedParts = append(uploadedParts, CompletePart{
PartNumber: pInfo.PartNumber,
ETag: pInfo.ETag,
})
}
_, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{})
if err != nil {
t.Fatalf("Failed to complete multipart upload - %v", err)
}
z := objLayer.(*erasureServerPools)
er := z.serverPools[0].sets[0]
erasureDisks := er.getDisks()
firstDisk := erasureDisks[0]
// Test 1: Remove the object backend files from the first disk.
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
}
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
if err != nil {
t.Fatalf("Failed to delete a file - %v", err)
}
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
if err != nil {
t.Fatalf("Failed to heal object - %v", err)
}
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
}
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
nfi1, err := getLatestFileInfo(ctx, fileInfos, errs)
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
}
if !reflect.DeepEqual(fi, nfi1) {
t.Fatalf("FileInfo not equal after healing")
}
// Test 2: Test with a corrupted xl.meta
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), []byte("abcd"))
if err != nil {
t.Errorf("Failure during creating part.1 - %v", err)
}
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
if err != nil {
t.Errorf("Expected nil but received %v", err)
}
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
nfi2, err := getLatestFileInfo(ctx, fileInfos, errs)
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
}
if !reflect.DeepEqual(fi, nfi2) {
t.Fatalf("FileInfo not equal after healing")
}
// Test 3: checks if HealObject returns an error when xl.meta is not found
// in more than read quorum number of disks, to create a corrupted situation.
for i := 0; i <= nfi2.Erasure.DataBlocks; i++ {
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
}
// Try healing now, expect to receive errFileNotFound.
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
if err != nil {
if _, ok := err.(ObjectNotFound); !ok {
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
}
}
// since majority of xl.meta's are not available, object should be successfully deleted.
_, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
if _, ok := err.(ObjectNotFound); !ok {
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
}
}
func TestHealObjectCorruptedParts(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -867,7 +1010,26 @@ func TestHealObjectCorrupted(t *testing.T) {
er := z.serverPools[0].sets[0]
erasureDisks := er.getDisks()
firstDisk := erasureDisks[0]
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
secondDisk := erasureDisks[1]
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
}
part1Disk1Origin, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil {
t.Fatalf("Failed to read a file - %v", err)
}
part1Disk2Origin, err := secondDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil {
t.Fatalf("Failed to read a file - %v", err)
}
// Test 1, remove part.1
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
if err != nil {
t.Fatalf("Failed to delete a file - %v", err)
}
@@ -877,85 +1039,67 @@ func TestHealObjectCorrupted(t *testing.T) {
t.Fatalf("Failed to heal object - %v", err)
}
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
part1Replaced, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
t.Fatalf("Failed to read a file - %v", err)
}
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) {
t.Fatalf("part.1 not healed correctly")
}
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
// Test 2, Corrupt part.1
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes"))
if err != nil {
t.Errorf("Failure during deleting part.1 - %v", err)
t.Fatalf("Failed to write a file - %v", err)
}
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte{})
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
if err != nil {
t.Errorf("Failure during creating part.1 - %v", err)
t.Fatalf("Failed to heal object - %v", err)
}
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
part1Replaced, err = firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil {
t.Errorf("Expected nil but received %v", err)
t.Fatalf("Failed to read a file - %v", err)
}
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
nfi, err := getLatestFileInfo(ctx, fileInfos, errs)
if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) {
t.Fatalf("part.1 not healed correctly")
}
// Test 3, Corrupt one part and remove data in another disk
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes"))
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
t.Fatalf("Failed to write a file - %v", err)
}
if !reflect.DeepEqual(fi, nfi) {
t.Fatalf("FileInfo not equal after healing")
}
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
err = secondDisk.Delete(context.Background(), bucket, object, true)
if err != nil {
t.Errorf("Failure during deleting part.1 - %v", err)
t.Fatalf("Failed to delete a file - %v", err)
}
bdata := bytes.Repeat([]byte("b"), int(nfi.Size))
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bdata)
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
if err != nil {
t.Errorf("Failure during creating part.1 - %v", err)
t.Fatalf("Failed to heal object - %v", err)
}
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
partReconstructed, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil {
t.Errorf("Expected nil but received %v", err)
t.Fatalf("Failed to read a file - %v", err)
}
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
nfi, err = getLatestFileInfo(ctx, fileInfos, errs)
if !reflect.DeepEqual(part1Disk1Origin, partReconstructed) {
t.Fatalf("part.1 not healed correctly")
}
partReconstructed, err = secondDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
if err != nil {
t.Fatalf("Failed to getLatestFileInfo - %v", err)
t.Fatalf("Failed to read a file - %v", err)
}
if !reflect.DeepEqual(fi, nfi) {
t.Fatalf("FileInfo not equal after healing")
}
// Test 4: checks if HealObject returns an error when xl.meta is not found
// in more than read quorum number of disks, to create a corrupted situation.
for i := 0; i <= nfi.Erasure.DataBlocks; i++ {
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
}
// Try healing now, expect to receive errFileNotFound.
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
if err != nil {
if _, ok := err.(ObjectNotFound); !ok {
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
}
}
// since majority of xl.meta's are not available, object should be successfully deleted.
_, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
if _, ok := err.(ObjectNotFound); !ok {
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
if !reflect.DeepEqual(part1Disk2Origin, partReconstructed) {
t.Fatalf("part.1 not healed correctly")
}
}
@@ -1017,7 +1161,8 @@ func TestHealObjectErasure(t *testing.T) {
t.Fatalf("Failed to complete multipart upload - %v", err)
}
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
// Delete the whole object folder
err = firstDisk.Delete(context.Background(), bucket, object, true)
if err != nil {
t.Fatalf("Failed to delete a file - %v", err)
}
+3
View File
@@ -67,6 +67,9 @@ func reduceErrs(errs []error, ignoredErrs []error) (maxCount int, maxErr error)
// values of maximally occurring errors validated against a generic
// quorum number that can be read or write quorum depending on usage.
func reduceQuorumErrs(ctx context.Context, errs []error, ignoredErrs []error, quorum int, quorumErr error) error {
if contextCanceled(ctx) {
return context.Canceled
}
maxCount, maxErr := reduceErrs(errs, ignoredErrs)
if maxCount >= quorum {
return maxErr
+12 -15
View File
@@ -343,10 +343,8 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
switch {
case errors.Is(err, errFileNotFound):
scan = madmin.HealNormalScan
logger.Info("Healing required, triggering async heal missing shards for %s", pathJoin(bucket, object, fi.VersionID))
case errors.Is(err, errFileCorrupt):
scan = madmin.HealDeepScan
logger.Info("Healing required, triggering async heal bitrot for %s", pathJoin(bucket, object, fi.VersionID))
}
switch scan {
case madmin.HealNormalScan, madmin.HealDeepScan:
@@ -495,11 +493,6 @@ func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object strin
return objInfo, toObjectErr(err, bucket, object)
}
objInfo = fi.ToObjectInfo(bucket, object)
if opts.VersionID != "" && !fi.VersionPurgeStatus().Empty() {
// Make sure to return object info to provide extra information.
return objInfo, toObjectErr(errMethodNotAllowed, bucket, object)
}
if fi.Deleted {
if opts.VersionID == "" || opts.DeleteMarker {
return objInfo, toObjectErr(errFileNotFound, bucket, object)
@@ -574,7 +567,7 @@ func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry str
// Wait for all renames to finish.
errs := g.Wait()
// We can safely allow RenameFile errors up to len(er.getDisks()) - writeQuorum
// We can safely allow RenameData errors up to len(er.getDisks()) - writeQuorum
// otherwise return failure. Cleanup successful renames.
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
return evalDisks(disks, errs), err
@@ -1001,14 +994,18 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
}
}
// Whether a disk was initially or becomes offline
// during this upload, send it to the MRF list.
for i := 0; i < len(onlineDisks); i++ {
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
continue
// For speedtest objects do not attempt to heal them.
if !opts.Speedtest {
// Whether a disk was initially or becomes offline
// during this upload, send it to the MRF list.
for i := 0; i < len(onlineDisks); i++ {
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
continue
}
er.addPartial(bucket, object, fi.VersionID, fi.Size)
break
}
er.addPartial(bucket, object, fi.VersionID, fi.Size)
break
}
fi.ReplicationState = opts.PutReplicationState()
+80
View File
@@ -128,6 +128,86 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
removeRoots(fsDirs)
}
func TestDeleteObjectsVersioned(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
obj, fsDirs, err := prepareErasure(ctx, 16)
if err != nil {
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
}
// Remove all dirs.
for _, dir := range fsDirs {
defer os.RemoveAll(dir)
}
type testCaseType struct {
bucket string
object string
}
bucketName := "bucket"
testCases := []testCaseType{
{bucketName, "dir/obj1"},
{bucketName, "dir/obj1"},
}
err = obj.MakeBucketWithLocation(ctx, bucketName, BucketOptions{
VersioningEnabled: true,
})
if err != nil {
t.Fatal(err)
}
names := make([]ObjectToDelete, len(testCases))
for i, testCase := range testCases {
objInfo, err := obj.PutObject(ctx, testCase.bucket, testCase.object,
mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{
Versioned: true,
})
if err != nil {
t.Fatalf("Erasure Object upload failed: <ERROR> %s", err)
}
names[i] = ObjectToDelete{
ObjectV: ObjectV{
ObjectName: objInfo.Name,
VersionID: objInfo.VersionID,
},
}
}
names = append(names, ObjectToDelete{
ObjectV: ObjectV{
ObjectName: "dir/obj1",
VersionID: mustGetUUID(), // add a non-existent UUID.
},
})
_, delErrs := obj.DeleteObjects(ctx, bucketName, names, ObjectOptions{
Versioned: true,
})
for i := range delErrs {
if delErrs[i] != nil {
t.Errorf("Failed to remove object `%v` with the error: `%v`", names[i], delErrs[i])
}
}
for i, test := range testCases {
_, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{
VersionID: names[i].ObjectV.VersionID,
})
switch statErr.(type) {
case VersionNotFound:
default:
t.Fatalf("Object %s is not removed", test.bucket+SlashSeparator+test.object)
}
}
if _, err = ioutil.ReadFile(pathJoin(fsDirs[0], bucketName, "dir/obj1", "xl.meta")); err == nil {
t.Fatalf("xl.meta still present after removal")
}
}
func TestErasureDeleteObjectsErasureSet(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
+27 -14
View File
@@ -472,13 +472,16 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
// '-1' as argument to decommission multiple pools at a time
// but this is not a priority at the moment.
for _, pool := range meta.returnResumablePools(1) {
err := z.Decommission(ctx, pool.ID)
switch err {
case errDecommissionAlreadyRunning:
fallthrough
case nil:
go z.doDecommissionInRoutine(ctx, pool.ID)
}
go func(pool PoolStatus) {
switch err := z.Decommission(ctx, pool.ID); err {
case errDecommissionAlreadyRunning:
fallthrough
case nil:
z.doDecommissionInRoutine(ctx, pool.ID)
default:
logger.LogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w", pool, err))
}
}(pool)
}
z.poolMeta = meta
@@ -593,7 +596,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
return
}
// We need a reversed order for Decommissioning,
// We need a reversed order for decommissioning,
// to create the appropriate stack.
versionsSorter(fivs.Versions).reverse()
@@ -801,12 +804,7 @@ func (z *erasureServerPools) getDecommissionPoolSpaceInfo(idx int) (pi poolSpace
if idx+1 > len(z.serverPools) {
return pi, errInvalidArgument
}
info, errs := z.serverPools[idx].StorageInfo(context.Background())
for _, err := range errs {
if err != nil {
return pi, errInvalidArgument
}
}
info, _ := z.serverPools[idx].StorageInfo(context.Background())
info.Backend = z.BackendInfo()
for _, disk := range info.Disks {
if disk.Healing {
@@ -955,6 +953,21 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
}
}
// Create .minio.sys/conifg, .minio.sys/buckets paths if missing,
// this code is present to avoid any missing meta buckets on other
// pools.
for _, metaBucket := range []string{
pathJoin(minioMetaBucket, minioConfigPrefix),
pathJoin(minioMetaBucket, bucketMetaPrefix),
} {
var bucketExists BucketExists
if err = z.MakeBucketWithLocation(ctx, metaBucket, BucketOptions{}); err != nil {
if !errors.As(err, &bucketExists) {
return err
}
}
}
// Buckets data are dispersed in multiple zones/sets, make
// sure to decommission the necessary metadata.
buckets = append(buckets, BucketInfo{
+65 -33
View File
@@ -33,6 +33,7 @@ import (
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio-go/v7/pkg/tags"
"github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/config/storageclass"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
@@ -128,7 +129,9 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
if !configRetriableErrors(err) {
logger.Fatal(err, "Unable to initialize backend")
}
time.Sleep(time.Duration(r.Float64() * float64(5*time.Second)))
retry := time.Duration(r.Float64() * float64(5*time.Second))
logger.LogIf(ctx, fmt.Errorf("Unable to initialize backend: %w, retrying in %s", err, retry))
time.Sleep(retry)
continue
}
break
@@ -661,6 +664,9 @@ func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket
for index := range z.serverPools {
index := index
g.Go(func() error {
if z.IsSuspended(index) {
return nil
}
return z.serverPools[index].MakeBucketWithLocation(ctx, bucket, opts)
}, index)
}
@@ -671,7 +677,10 @@ func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket
if err != nil {
if _, ok := err.(BucketExists); !ok {
// Delete created buckets, ignoring errors.
z.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{Force: true, NoRecreate: true})
z.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
Force: false,
NoRecreate: true,
})
}
return err
}
@@ -811,6 +820,11 @@ func (z *erasureServerPools) getLatestObjectInfoWithIdx(ctx context.Context, buc
// should be returned upwards.
return res.oi, res.zIdx, err
}
// When its a delete marker and versionID is empty
// we should simply return the error right away.
if res.oi.DeleteMarker && opts.VersionID == "" {
return res.oi, res.zIdx, err
}
}
object = decodeDirObject(object)
@@ -1144,6 +1158,14 @@ func maxKeysPlusOne(maxKeys int, addOne bool) int {
func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
var loi ListObjectsInfo
// Automatically remove the object/version is an expiry lifecycle rule can be applied
lc, _ := globalLifecycleSys.Get(bucket)
if lc != nil {
if !lc.HasActiveRules(prefix, true) {
lc = nil
}
}
if len(prefix) > 0 && maxKeys == 1 && delimiter == "" && marker == "" {
// Optimization for certain applications like
// - Cohesity
@@ -1154,6 +1176,13 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
// to avoid the need for ListObjects().
objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
if err == nil {
if lc != nil {
action := evalActionFromLifecycle(ctx, *lc, objInfo, false)
switch action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
return loi, nil
}
}
loi.Objects = append(loi.Objects, objInfo)
return loi, nil
}
@@ -1168,6 +1197,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
InclDeleted: false,
AskDisks: globalAPIConfig.getListQuorum(),
}
merged, err := z.listPath(ctx, &opts)
if err != nil && err != io.EOF {
if !isErrBucketNotFound(err) {
@@ -1476,6 +1506,9 @@ func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, op
for index := range z.serverPools {
index := index
g.Go(func() error {
if z.IsSuspended(index) {
return nil
}
return z.serverPools[index].DeleteBucket(ctx, bucket, opts)
}, index)
}
@@ -1730,14 +1763,13 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
// HealObjectFn closure function heals the object.
type HealObjectFn func(bucket, object, versionID string) error
func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects, healEntry func(metaCacheEntry) error, errCh chan<- error) {
func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects, healEntry func(metaCacheEntry) error) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
disks, _ := set.getOnlineDisksWithHealing()
if len(disks) == 0 {
errCh <- errors.New("listAndHeal: No non-healing disks found")
return
return errors.New("listAndHeal: No non-healing disks found")
}
// How to resolve partial results.
@@ -1763,8 +1795,8 @@ func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects
reportNotFound: false,
agreed: func(entry metaCacheEntry) {
if err := healEntry(entry); err != nil {
errCh <- err
return
logger.LogIf(ctx, err)
cancel()
}
},
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
@@ -1776,7 +1808,8 @@ func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects
}
if err := healEntry(*entry); err != nil {
errCh <- err
logger.LogIf(ctx, err)
cancel()
return
}
},
@@ -1784,13 +1817,13 @@ func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects
}
if err := listPathRaw(ctx, lopts); err != nil {
errCh <- fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts)
return
return fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts)
}
return nil
}
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error {
errCh := make(chan error)
healEntry := func(entry metaCacheEntry) error {
if entry.isDir() {
return nil
@@ -1829,34 +1862,33 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
defer close(errCh)
var poolErrs [][]error
for idx, erasureSet := range z.serverPools {
if z.IsSuspended(idx) {
continue
}
errs := make([]error, len(erasureSet.sets))
var wg sync.WaitGroup
for idx, erasureSet := range z.serverPools {
if z.IsSuspended(idx) {
continue
}
for _, set := range erasureSet.sets {
wg.Add(1)
go func(set *erasureObjects) {
defer wg.Done()
for idx, set := range erasureSet.sets {
wg.Add(1)
go func(idx int, set *erasureObjects) {
defer wg.Done()
listAndHeal(ctx, bucket, prefix, set, healEntry, errCh)
}(set)
}
errs[idx] = listAndHeal(ctx, bucket, prefix, set, healEntry)
}(idx, set)
}
wg.Wait()
}()
var err error
for e := range errCh {
// Save first non-nil error.
if e != nil && err != nil {
err = e
cancel()
poolErrs = append(poolErrs, errs)
}
for _, errs := range poolErrs {
for _, err := range errs {
if err == nil {
continue
}
return err
}
}
return err
return nil
}
func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
+20 -25
View File
@@ -97,21 +97,6 @@ type erasureSets struct {
lastConnectDisksOpTime time.Time
}
// Return false if endpoint is not connected or has been reconnected after last check
func isEndpointConnectionStable(diskMap map[Endpoint]StorageAPI, endpoint Endpoint, lastCheck time.Time) bool {
disk := diskMap[endpoint]
if disk == nil {
return false
}
if !disk.IsOnline() {
return false
}
if !lastCheck.IsZero() && disk.LastConn().After(lastCheck) {
return false
}
return true
}
func (s *erasureSets) getDiskMap() map[Endpoint]StorageAPI {
diskMap := make(map[Endpoint]StorageAPI)
@@ -209,12 +194,26 @@ func (s *erasureSets) connectDisks() {
}()
var wg sync.WaitGroup
setsJustConnected := make([]bool, s.setCount)
diskMap := s.getDiskMap()
setsJustConnected := make([]bool, s.setCount)
for _, endpoint := range s.endpoints.Endpoints {
if isEndpointConnectionStable(diskMap, endpoint, s.lastConnectDisksOpTime) {
continue
cdisk := diskMap[endpoint]
if cdisk != nil && cdisk.IsOnline() {
if s.lastConnectDisksOpTime.IsZero() {
continue
}
// An online-disk means its a valid disk but it may be a re-connected disk
// we verify that here based on LastConn(), however we make sure to avoid
// putting it back into the s.erasureDisks by re-placing the disk again.
_, setIndex, _ := cdisk.GetDiskLoc()
if setIndex != -1 {
// Recently disconnected disks must go to MRF
setsJustConnected[setIndex] = cdisk.LastConn().After(s.lastConnectDisksOpTime)
continue
}
}
wg.Add(1)
go func(endpoint Endpoint) {
defer wg.Done()
@@ -222,7 +221,6 @@ func (s *erasureSets) connectDisks() {
if err != nil {
if endpoint.IsLocal && errors.Is(err, errUnformattedDisk) {
globalBackgroundHealState.pushHealLocalDisks(endpoint)
logger.Info(fmt.Sprintf("Found unformatted drive %s, attempting to heal...", endpoint))
} else {
printEndpointError(endpoint, err, true)
}
@@ -230,7 +228,6 @@ func (s *erasureSets) connectDisks() {
}
if disk.IsLocal() && disk.Healing() != nil {
globalBackgroundHealState.pushHealLocalDisks(disk.Endpoint())
logger.Info(fmt.Sprintf("Found the drive %s that needs healing, attempting to heal...", disk))
}
s.erasureDisksMu.RLock()
setIndex, diskIndex, err := findDiskIndex(s.format, format)
@@ -268,7 +265,7 @@ func (s *erasureSets) connectDisks() {
s.erasureDisks[setIndex][diskIndex] = disk
}
disk.SetDiskLoc(s.poolIndex, setIndex, diskIndex)
setsJustConnected[setIndex] = true
setsJustConnected[setIndex] = true // disk just went online we treat it is as MRF event
s.erasureDisksMu.Unlock()
}(endpoint)
}
@@ -280,7 +277,7 @@ func (s *erasureSets) connectDisks() {
if !justConnected {
continue
}
globalMRFState.newSetReconnected(setIndex, s.poolIndex)
globalMRFState.newSetReconnected(s.poolIndex, setIndex)
}
}()
}
@@ -1257,9 +1254,7 @@ func markRootDisksAsDown(storageDisks []StorageAPI, errs []error) {
if storageDisks[i] != nil && infos[i].RootDisk {
// We should not heal on root disk. i.e in a situation where the minio-administrator has unmounted a
// defective drive we should not heal a path on the root disk.
logger.Info("Disk `%s` the same as the system root disk.\n"+
"Disk will not be used. Please supply a separate disk and restart the server.",
storageDisks[i].String())
logger.LogIf(GlobalContext, fmt.Errorf("Disk `%s` is part of root disk, will not be used", storageDisks[i]))
storageDisks[i] = nil
}
}
+1 -2
View File
@@ -29,7 +29,6 @@ import (
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/bpool"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/dsync"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/sync/errgroup"
@@ -353,7 +352,7 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, bf
// Collect disks we can use.
disks, healing := er.getOnlineDisksWithHealing()
if len(disks) == 0 {
logger.Info(color.Green("data-scanner:") + " all disks are offline or being healed, skipping scanner")
logger.LogIf(ctx, errors.New("data-scanner: all disks are offline or being healed, skipping scanner cycle"))
return nil
}
+2
View File
@@ -335,6 +335,8 @@ func ErrorRespToObjectError(err error, params ...string) error {
switch minioErr.StatusCode {
case http.StatusMethodNotAllowed:
err = toObjectErr(errMethodNotAllowed, bucket, object)
case http.StatusBadGateway:
return BackendDown{Err: err.Error()}
}
return err
}
+3 -3
View File
@@ -161,7 +161,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
// This is only to uniquely identify each gateway deployments.
globalDeploymentID = env.Get("MINIO_GATEWAY_DEPLOYMENT_ID", mustGetUUID())
logger.SetDeploymentID(globalDeploymentID)
xhttp.SetDeploymentID(globalDeploymentID)
if gw == nil {
logger.FatalIf(errUnexpected, "Gateway implementation not initialized")
@@ -176,7 +176,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
// Initialize globalConsoleSys system
globalConsoleSys = NewConsoleLogger(GlobalContext)
logger.AddTarget(globalConsoleSys)
logger.AddSystemTarget(globalConsoleSys)
// Handle common command args.
handleCommonCmdArgs(ctx)
@@ -377,7 +377,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
}
// TODO: remove the following line by June 1st.
logStartupMessage(
logger.Info(
color.RedBold(`
===================================================================================
**** WARNING: MinIO Gateway will be removed by June 1st from MinIO repository *****
+8 -7
View File
@@ -22,6 +22,7 @@ import (
"strings"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/logger"
)
// Prints the formatted startup message.
@@ -45,7 +46,7 @@ func printGatewayStartupMessage(apiEndPoints []string, backendType string) {
if globalMinioConsolePortAuto && globalBrowserEnabled {
msg := fmt.Sprintf("\nWARNING: Console endpoint is listening on a dynamic port (%s), please use --console-address \":PORT\" to choose a static port.",
globalMinioConsolePort)
logStartupMessage(color.RedBold(msg))
logger.Info(color.RedBold(msg))
}
}
@@ -57,19 +58,19 @@ func printGatewayCommonMsg(apiEndpoints []string) {
apiEndpointStr := strings.Join(apiEndpoints, " ")
// Colorize the message and print.
logStartupMessage(color.Blue("API: ") + color.Bold(fmt.Sprintf("%s ", apiEndpointStr)))
logger.Info(color.Blue("API: ") + color.Bold(fmt.Sprintf("%s ", apiEndpointStr)))
if color.IsTerminal() && !globalCLIContext.Anonymous {
logStartupMessage(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logStartupMessage(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
logger.Info(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logger.Info(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
}
printEventNotifiers()
if globalBrowserEnabled {
consoleEndpointStr := strings.Join(stripStandardPorts(getConsoleEndpoints(), globalMinioConsoleHost), " ")
logStartupMessage(color.Blue("\nConsole: ") + color.Bold(fmt.Sprintf("%s ", consoleEndpointStr)))
logger.Info(color.Blue("\nConsole: ") + color.Bold(fmt.Sprintf("%s ", consoleEndpointStr)))
if color.IsTerminal() && !globalCLIContext.Anonymous {
logStartupMessage(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logStartupMessage(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
logger.Info(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logger.Info(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
}
}
}
-2
View File
@@ -330,8 +330,6 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
default:
tracker.bucketDone(bucket)
logger.LogIf(ctx, tracker.update(ctx))
logger.Info("Healing bucket %s content on %s erasure set complete",
bucket, humanize.Ordinal(tracker.SetIndex+1))
}
}
tracker.Object = ""
+7 -2
View File
@@ -102,7 +102,7 @@ const (
GlobalStaleUploadsCleanupInterval = time.Hour * 6 // 6 hrs.
// Refresh interval to update in-memory iam config cache.
globalRefreshIAMInterval = 5 * time.Minute
globalRefreshIAMInterval = 10 * time.Minute
// Limit of location constraint XML for unauthenticated PUT bucket operations.
maxLocationConstraintSize = 3 * humanize.MiByte
@@ -190,7 +190,7 @@ var (
globalBucketTargetSys *BucketTargetSys
// globalAPIConfig controls S3 API requests throttling,
// healthcheck readiness deadlines and cors settings.
globalAPIConfig = apiConfig{listQuorum: 3}
globalAPIConfig = apiConfig{listQuorum: "strict"}
globalStorageClass storageclass.Config
globalLDAPConfig xldap.Config
@@ -345,6 +345,11 @@ var (
globalIsCICD bool
globalRootDiskThreshold uint64
// Used for collecting stats for netperf
globalNetPerfMinDuration = time.Second * 10
globalNetPerfRX netPerfRX
// Add new variable global values here.
)
+4 -4
View File
@@ -38,7 +38,7 @@ type apiConfig struct {
requestsDeadline time.Duration
requestsPool chan struct{}
clusterDeadline time.Duration
listQuorum int
listQuorum string
corsAllowOrigins []string
// total drives per erasure set across pools.
totalDriveCount int
@@ -127,7 +127,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
}
}
if cap(t.requestsPool) < apiRequestsMaxPerNode {
if cap(t.requestsPool) != apiRequestsMaxPerNode {
// Only replace if needed.
// Existing requests will use the previous limit,
// but new requests will use the new limit.
@@ -136,7 +136,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
t.requestsPool = make(chan struct{}, apiRequestsMaxPerNode)
}
t.requestsDeadline = cfg.RequestsDeadline
t.listQuorum = cfg.GetListQuorum()
t.listQuorum = cfg.ListQuorum
if globalReplicationPool != nil &&
cfg.ReplicationWorkers != t.replicationWorkers {
globalReplicationPool.ResizeFailedWorkers(cfg.ReplicationFailedWorkers)
@@ -170,7 +170,7 @@ func (t *apiConfig) shouldGzipObjects() bool {
return t.gzipObjects
}
func (t *apiConfig) getListQuorum() int {
func (t *apiConfig) getListQuorum() string {
t.mu.RLock()
defer t.mu.RUnlock()
+23
View File
@@ -174,3 +174,26 @@ func (h *HTTPRangeSpec) String(resourceSize int64) string {
}
return fmt.Sprintf("%d-%d", off, off+length-1)
}
// ToHeader returns the Range header value.
func (h *HTTPRangeSpec) ToHeader() (string, error) {
if h == nil {
return "", nil
}
start := strconv.Itoa(int(h.Start))
end := strconv.Itoa(int(h.End))
switch {
case h.Start >= 0 && h.End >= 0:
if h.Start > h.End {
return "", errInvalidRange
}
case h.IsSuffixLength:
end = strconv.Itoa(int(h.Start * -1))
start = ""
case h.Start > -1:
end = ""
default:
return "", fmt.Errorf("does not have valid range value")
}
return fmt.Sprintf("bytes=%s-%s", start, end), nil
}
+43
View File
@@ -101,3 +101,46 @@ func TestHTTPRequestRangeSpec(t *testing.T) {
t.Errorf("Case %d: Expected errInvalidRange but: %v %v %d %d %v", i, rs, err1, o, l, err2)
}
}
func TestHTTPRequestRangeToHeader(t *testing.T) {
validRangeSpecs := []struct {
spec string
errExpected bool
}{
{"bytes=0-", false},
{"bytes=1-", false},
{"bytes=0-9", false},
{"bytes=1-10", false},
{"bytes=1-1", false},
{"bytes=2-5", false},
{"bytes=-5", false},
{"bytes=-1", false},
{"bytes=-1000", false},
{"bytes=", true},
{"bytes= ", true},
{"byte=", true},
{"bytes=A-B", true},
{"bytes=1-B", true},
{"bytes=B-1", true},
{"bytes=-1-1", true},
}
for i, testCase := range validRangeSpecs {
rs, err := parseRequestRangeSpec(testCase.spec)
if err != nil {
if !testCase.errExpected || err == nil && testCase.errExpected {
t.Errorf("unexpected err: %v", err)
}
continue
}
h, err := rs.ToHeader()
if err != nil && !testCase.errExpected || err == nil && testCase.errExpected {
t.Errorf("expected error with invalid range: %v", err)
}
if h != testCase.spec {
t.Errorf("Case %d: translated to incorrect header: %s expected: %s",
i, h, testCase.spec)
}
}
}
+131
View File
@@ -19,6 +19,7 @@ package cmd
import (
"context"
"fmt"
"path"
"strings"
"sync"
@@ -405,6 +406,136 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IA
return nil
}
var (
usersListKey = "/users/"
svcAccListKey = "/service-accounts/"
groupsListKey = "/groups/"
policiesListKey = "/policies/"
stsListKey = "/sts/"
policyDBUsersListKey = "/policydb/users/"
policyDBSTSUsersListKey = "/policydb/sts-users/"
policyDBServiceAccountsListKey = "/policydb/service-accounts/"
policyDBGroupsListKey = "/policydb/groups/"
allListKeys = []string{
usersListKey,
svcAccListKey,
groupsListKey,
policiesListKey,
stsListKey,
policyDBUsersListKey,
policyDBSTSUsersListKey,
policyDBServiceAccountsListKey,
policyDBGroupsListKey,
}
)
func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[string][]string, error) {
res := make(map[string][]string)
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix) {
if item.Err != nil {
return nil, item.Err
}
found := false
for _, listKey := range allListKeys {
if strings.HasPrefix(item.Item, listKey) {
found = true
name := strings.TrimPrefix(item.Item, listKey)
res[listKey] = append(res[listKey], name)
break
}
}
if !found && !(item.Item == "config/config.json" || item.Item == "/format.json") {
logger.LogIf(ctx, fmt.Errorf("unknown type of IAM file listed: %v", item.Item))
}
}
return res, nil
}
// Assumes cache is locked by caller.
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error {
listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
if err != nil {
return err
}
// Loads things in the same order as `LoadIAMCache()`
policiesList := listedConfigItems[policiesListKey]
for _, item := range policiesList {
policyName := path.Dir(item)
if err := iamOS.loadPolicyDoc(ctx, policyName, cache.iamPolicyDocsMap); err != nil && err != errNoSuchPolicy {
return err
}
}
setDefaultCannedPolicies(cache.iamPolicyDocsMap)
if iamOS.usersSysType == MinIOUsersSysType {
regUsersList := listedConfigItems[usersListKey]
for _, item := range regUsersList {
userName := path.Dir(item)
if err := iamOS.loadUser(ctx, userName, regUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
return err
}
}
groupsList := listedConfigItems[groupsListKey]
for _, item := range groupsList {
group := path.Dir(item)
if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup {
return err
}
}
}
userPolicyMappingsList := listedConfigItems[policyDBUsersListKey]
for _, item := range userPolicyMappingsList {
userName := strings.TrimSuffix(item, ".json")
if err := iamOS.loadMappedPolicy(ctx, userName, regUser, false, cache.iamUserPolicyMap); err != nil && err != errNoSuchPolicy {
return err
}
}
groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
for _, item := range groupPolicyMappingsList {
groupName := strings.TrimSuffix(item, ".json")
if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && err != errNoSuchPolicy {
return err
}
}
svcAccList := listedConfigItems[svcAccListKey]
for _, item := range svcAccList {
userName := path.Dir(item)
if err := iamOS.loadUser(ctx, userName, svcUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
return err
}
}
stsUsersList := listedConfigItems[stsListKey]
for _, item := range stsUsersList {
userName := path.Dir(item)
if err := iamOS.loadUser(ctx, userName, stsUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
return err
}
}
stsPolicyMappingsList := listedConfigItems[policyDBSTSUsersListKey]
for _, item := range stsPolicyMappingsList {
stsName := strings.TrimSuffix(item, ".json")
if err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, cache.iamUserPolicyMap); err != nil && err != errNoSuchPolicy {
return err
}
}
cache.buildUserGroupMemberships()
return nil
}
func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error {
return iamOS.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName))
}
+84 -51
View File
@@ -432,48 +432,56 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
cache := store.lock()
defer store.unlock()
if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
return err
}
// Sets default canned policies, if none are set.
setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
if store.getUsersSysType() == MinIOUsersSysType {
if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
err := iamOS.loadAllFromObjStore(ctx, newCache)
if err != nil {
return err
}
if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
} else {
if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
return err
}
}
// load polices mapped to users
if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
return err
}
// Sets default canned policies, if none are set.
setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
// load policies mapped to groups
if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
return err
}
if store.getUsersSysType() == MinIOUsersSysType {
if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
return err
}
if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
return err
}
}
// load service accounts
if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
return err
}
// load polices mapped to users
if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
return err
}
// load STS temp users
if err := store.loadUsers(ctx, stsUser, newCache.iamUsersMap); err != nil {
return err
}
// load policies mapped to groups
if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
return err
}
// load STS policy mappings
if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamUserPolicyMap); err != nil {
return err
}
// load service accounts
if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
return err
}
newCache.buildUserGroupMemberships()
// load STS temp users
if err := store.loadUsers(ctx, stsUser, newCache.iamUsersMap); err != nil {
return err
}
// load STS policy mappings
if err := store.loadMappedPolicies(ctx, stsUser, false, newCache.iamUserPolicyMap); err != nil {
return err
}
newCache.buildUserGroupMemberships()
}
cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap
cache.iamGroupsMap = newCache.iamGroupsMap
@@ -1450,19 +1458,34 @@ func (store *IAMStoreSys) DeleteUsers(ctx context.Context, users []string) error
// GetAllParentUsers - returns all distinct "parent-users" associated with STS or service
// credentials.
func (store *IAMStoreSys) GetAllParentUsers() []string {
func (store *IAMStoreSys) GetAllParentUsers() map[string]string {
cache := store.rlock()
defer store.runlock()
res := set.NewStringSet()
res := map[string]string{}
for _, cred := range cache.iamUsersMap {
if cred.IsServiceAccount() || cred.IsTemp() {
if (cred.IsServiceAccount() || cred.IsTemp()) && cred.SessionToken != "" {
parentUser := cred.ParentUser
if cred.SessionToken != "" {
claims, err := getClaimsFromToken(cred.SessionToken)
var (
err error
claims map[string]interface{}
)
if cred.IsServiceAccount() {
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, cred.SecretKey)
if err != nil {
continue
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, globalActiveCred.SecretKey)
}
} else if cred.IsTemp() {
claims, err = getClaimsFromTokenWithSecret(cred.SessionToken, globalActiveCred.SecretKey)
}
if err != nil {
continue
}
if len(claims) > 0 {
if v, ok := claims[subClaim]; ok {
subFromToken, ok := v.(string)
if ok {
@@ -1470,11 +1493,18 @@ func (store *IAMStoreSys) GetAllParentUsers() []string {
}
}
}
res.Add(parentUser)
if parentUser == "" {
continue
}
if _, ok := res[parentUser]; !ok {
res[parentUser] = cred.ParentUser
}
}
}
return res.ToSlice()
return res
}
// SetUserStatus - sets current user status.
@@ -1559,6 +1589,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
return errNoSuchServiceAccount
}
currentSecretKey := cr.SecretKey
if opts.secretKey != "" {
if !auth.IsSecretKeyValid(opts.secretKey) {
return auth.ErrInvalidSecretKeyLength
@@ -1580,20 +1611,21 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
return errors.New("unknown account status value")
}
if opts.sessionPolicy != nil {
m, err := getClaimsFromToken(cr.SessionToken)
if err != nil {
return fmt.Errorf("unable to get svc acc claims: %v", err)
}
m, err := getClaimsFromTokenWithSecret(cr.SessionToken, currentSecretKey)
if err != nil {
return fmt.Errorf("unable to get svc acc claims: %v", err)
}
err = opts.sessionPolicy.Validate()
if err != nil {
if opts.sessionPolicy != nil {
if err := opts.sessionPolicy.Validate(); err != nil {
return err
}
policyBuf, err := json.Marshal(opts.sessionPolicy)
if err != nil {
return err
}
if len(policyBuf) > 16*humanize.KiByte {
return fmt.Errorf("Session policy should not exceed 16 KiB characters")
}
@@ -1601,10 +1633,11 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
// Overwrite session policy claims.
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
m[iamPolicyClaimNameSA()] = "embedded-policy"
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey)
if err != nil {
return err
}
}
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, cr.SecretKey)
if err != nil {
return err
}
u := newUserIdentity(cr)
+62 -19
View File
@@ -37,6 +37,7 @@ import (
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
etcd "go.etcd.io/etcd/client/v3"
@@ -198,6 +199,8 @@ func (sys *IAMSys) Load(ctx context.Context) error {
// Init - initializes config system by reading entries from config/iam
func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etcd.Client, iamRefreshInterval time.Duration) {
iamInitStart := time.Now()
sys.Lock()
defer sys.Unlock()
@@ -268,6 +271,8 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
break
}
iamLoadStart := time.Now()
// Load IAM data from storage.
for {
if err := sys.Load(retryCtx); err != nil {
@@ -332,6 +337,9 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
}
sys.printIAMRoles()
now := time.Now()
logger.Info("Finished loading IAM sub-system (took %.1fs of %.1fs to load data).", now.Sub(iamLoadStart).Seconds(), now.Sub(iamInitStart).Seconds())
}
// Prints IAM role ARNs.
@@ -349,7 +357,7 @@ func (sys *IAMSys) printIAMRoles() {
msgs = append(msgs, color.Bold(arn))
}
logStartupMessage(fmt.Sprintf("%s %s", color.Blue("IAM Roles:"), strings.Join(msgs, " ")))
logger.Info(fmt.Sprintf("%s %s", color.Blue("IAM Roles:"), strings.Join(msgs, " ")))
}
// HasWatcher - returns if the IAM system has a watcher to be notified of
@@ -365,20 +373,30 @@ func (sys *IAMSys) watch(ctx context.Context) {
for event := range ch {
// we simply log errors
err := sys.loadWatchedEvent(ctx, event)
logger.LogIf(ctx, err)
logger.LogIf(ctx, fmt.Errorf("Failure in loading watch event: %v", err))
}
return
}
var maxRefreshDurationSecondsForLog float64 = 10
// Fall back to loading all items periodically
ticker := time.NewTicker(sys.iamRefreshInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
refreshStart := time.Now()
if err := sys.Load(ctx); err != nil {
logger.LogIf(ctx, err)
logger.LogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err))
} else {
took := time.Since(refreshStart).Seconds()
if took > maxRefreshDurationSecondsForLog {
// Log if we took a lot of time to load.
logger.Info("IAM refresh took %.2fs", took)
}
}
case <-ctx.Done():
return
}
@@ -799,14 +817,17 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
}
}
var cred auth.Credentials
var accessKey, secretKey string
var err error
if len(opts.accessKey) > 0 {
cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey)
accessKey, secretKey = opts.accessKey, opts.secretKey
} else {
cred, err = auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
accessKey, secretKey, err = auth.GenerateCredentials()
if err != nil {
return auth.Credentials{}, err
}
}
cred, err := auth.CreateNewCredentialsWithMetadata(accessKey, secretKey, m, secretKey)
if err != nil {
return auth.Credentials{}, err
}
@@ -880,9 +901,12 @@ func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (aut
var embeddedPolicy *iampolicy.Policy
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, sa.SecretKey)
if err != nil {
return auth.Credentials{}, nil, err
jwtClaims, err = auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
if err != nil {
return auth.Credentials{}, nil, err
}
}
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName)
@@ -915,9 +939,12 @@ func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (ma
return nil, errNoSuchServiceAccount
}
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
jwtClaims, err := auth.ExtractClaims(sa.SessionToken, sa.SecretKey)
if err != nil {
return nil, err
jwtClaims, err = auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey)
if err != nil {
return nil, err
}
}
return jwtClaims.Map(), nil
}
@@ -1003,7 +1030,7 @@ func (sys *IAMSys) SetUserSecretKey(ctx context.Context, accessKey string, secre
func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
parentUsers := sys.store.GetAllParentUsers()
var expiredUsers []string
for _, parentUser := range parentUsers {
for parentUser, expiredUser := range parentUsers {
u, err := globalOpenIDConfig.LookupUser(parentUser)
if err != nil {
logger.LogIf(GlobalContext, err)
@@ -1012,7 +1039,7 @@ func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
// If user is set to "disabled", we will remove them
// subsequently.
if !u.Enabled {
expiredUsers = append(expiredUsers, parentUser)
expiredUsers = append(expiredUsers, expiredUser)
}
}
@@ -1025,12 +1052,12 @@ func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) {
parentUsers := sys.store.GetAllParentUsers()
var allDistNames []string
for _, parentUser := range parentUsers {
for parentUser, expiredUser := range parentUsers {
if !globalLDAPConfig.IsLDAPUserDN(parentUser) {
continue
}
allDistNames = append(allDistNames, parentUser)
allDistNames = append(allDistNames, expiredUser)
}
expiredUsers, err := globalLDAPConfig.GetNonEligibleUserDistNames(allDistNames)
@@ -1063,12 +1090,28 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
// Try to find the ldapUsername for this
// parentUser by extracting JWT claims
jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
if err != nil {
// skip this cred - session token seems
// invalid
var (
jwtClaims *jwt.MapClaims
err error
)
if cred.SessionToken == "" {
continue
}
if cred.IsServiceAccount() {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, cred.SecretKey)
if err != nil {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
}
} else {
jwtClaims, err = auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
}
if err != nil {
// skip this cred - session token seems invalid
continue
}
ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
if !ok {
// skip this cred - we dont have the
+1 -1
View File
@@ -132,7 +132,7 @@ func authenticateURL(accessKey, secretKey string) (string, error) {
// Check if the request is authenticated.
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors.
func webRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, bool, error) {
token, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
if err != nil {
if err == jwtreq.ErrNoTokenInRequest {
+1 -1
View File
@@ -149,7 +149,7 @@ func TestWebRequestAuthenticate(t *testing.T) {
}
for i, testCase := range testCases {
_, _, _, gotErr := webRequestAuthenticate(testCase.req)
_, _, _, gotErr := metricsRequestAuthenticate(testCase.req)
if testCase.expectedErr != gotErr {
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
}
+2 -2
View File
@@ -44,7 +44,7 @@ var GlobalFlags = []cli.Flag{
},
cli.BoolFlag{
Name: "quiet",
Usage: "disable startup information",
Usage: "disable startup and info messages",
},
cli.BoolFlag{
Name: "anonymous",
@@ -52,7 +52,7 @@ var GlobalFlags = []cli.Flag{
},
cli.BoolFlag{
Name: "json",
Usage: "output server logs and startup information in json format",
Usage: "output logs in JSON format",
},
// Deprecated flag, so its hidden now, existing deployments will keep working.
cli.BoolFlag{
+2 -2
View File
@@ -42,7 +42,7 @@ func Benchmark_bucketMetacache_findCache(b *testing.B) {
FilterPrefix: "",
Marker: "",
Limit: 0,
AskDisks: 0,
AskDisks: "strict",
Recursive: false,
Separator: slashSeparator,
Create: true,
@@ -59,7 +59,7 @@ func Benchmark_bucketMetacache_findCache(b *testing.B) {
FilterPrefix: "",
Marker: "",
Limit: 0,
AskDisks: 0,
AskDisks: "strict",
Recursive: false,
Separator: slashSeparator,
Create: true,
+24 -8
View File
@@ -191,6 +191,16 @@ func (e *metaCacheEntry) fileInfo(bucket string) (FileInfo, error) {
}, nil
}
if e.cached != nil {
if len(e.cached.versions) == 0 {
// This special case is needed to handle xlMeta.versions == 0
return FileInfo{
Volume: bucket,
Name: e.name,
Deleted: true,
IsLatest: true,
ModTime: timeSentinel1970,
}, nil
}
return e.cached.ToFileInfo(bucket, e.name, "")
}
return getFileInfo(e.metadata, bucket, e.name, "", false)
@@ -271,10 +281,16 @@ func (m metaCacheEntries) shallowClone() metaCacheEntries {
}
type metadataResolutionParams struct {
dirQuorum int // Number if disks needed for a directory to 'exist'.
objQuorum int // Number of disks needed for an object to 'exist'.
bucket string // Name of the bucket. Used for generating cached fileinfo.
strict bool // Versions must match exactly, including all metadata.
dirQuorum int // Number if disks needed for a directory to 'exist'.
objQuorum int // Number of disks needed for an object to 'exist'.
// An optimization request only an 'n' amount of versions from xl.meta
// to avoid resolving all versions to figure out the latest 'version'
// for ListObjects, ListObjectsV2
requestedVersions int
bucket string // Name of the bucket. Used for generating cached fileinfo.
strict bool // Versions must match exactly, including all metadata.
// Reusable slice for resolution
candidates [][]xlMetaV2ShallowVersion
@@ -362,7 +378,7 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
reusable: true,
cached: &xlMetaV2{metaV: selected.cached.metaV},
}
selected.cached.versions = mergeXLV2Versions(r.objQuorum, r.strict, r.candidates...)
selected.cached.versions = mergeXLV2Versions(r.objQuorum, r.strict, r.requestedVersions, r.candidates...)
if len(selected.cached.versions) == 0 {
return nil, false
}
@@ -379,11 +395,11 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
// firstFound returns the first found and the number of set entries.
func (m metaCacheEntries) firstFound() (first *metaCacheEntry, n int) {
for _, entry := range m {
for i, entry := range m {
if entry.name != "" {
n++
if first == nil {
first = &entry
first = &m[i]
}
}
}
@@ -488,7 +504,7 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
return versions
}
// fileInfoVersions converts the metadata to FileInfoVersions where possible.
// fileInfos converts the metadata to ObjectInfo where possible.
// Metadata that cannot be decoded is skipped.
func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (objects []ObjectInfo) {
objects = make([]ObjectInfo, 0, m.len())
+55 -9
View File
@@ -28,6 +28,7 @@ import (
"sync"
"time"
"github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/logger"
)
@@ -110,7 +111,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
// If we don't have a list id we must ask the server if it has a cache or create a new.
if o.ID != "" && !o.Transient {
// Create or ping with handout...
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var c *metacache
@@ -198,7 +199,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
}
entries.truncate(0)
go func() {
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
if rpc != nil {
ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
defer cancel()
@@ -289,6 +290,14 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
}
mu.Unlock()
// Do lifecycle filtering.
if o.lcFilter != nil {
filterIn := make(chan metaCacheEntry, 10)
go filterLifeCycle(ctx, o.Bucket, o.lcFilter, filterIn, results)
// Replace results.
results = filterIn
}
// Gather results to a single channel.
err := mergeEntryChannels(ctx, inputs, results, func(existing, other *metaCacheEntry) (replace bool) {
// Pick object over directory
@@ -298,21 +307,20 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
if !existing.isDir() && other.isDir() {
return false
}
eFIV, err := existing.fileInfo(o.Bucket)
eMeta, err := existing.xlmeta()
if err != nil {
return true
}
oFIV, err := other.fileInfo(o.Bucket)
oMeta, err := other.xlmeta()
if err != nil {
return false
}
// Replace if modtime is newer
if !oFIV.ModTime.Equal(eFIV.ModTime) {
return oFIV.ModTime.After(eFIV.ModTime)
if !oMeta.latestModtime().Equal(oMeta.latestModtime()) {
return oMeta.latestModtime().After(eMeta.latestModtime())
}
// Use NumVersions as a final tiebreaker.
return oFIV.NumVersions > eFIV.NumVersions
return len(oMeta.versions) > len(eMeta.versions)
})
cancelList()
@@ -346,6 +354,44 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
return nil
}
// filterLifeCycle will filter out objects if the most recent
// version should be deleted by lifecycle.
// out will be closed when there are no more results.
// When 'in' is closed or the context is canceled the
// function closes 'out' and exits.
func filterLifeCycle(ctx context.Context, bucket string, lc *lifecycle.Lifecycle, in <-chan metaCacheEntry, out chan<- metaCacheEntry) {
defer close(out)
for {
var obj metaCacheEntry
var ok bool
select {
case <-ctx.Done():
return
case obj, ok = <-in:
if !ok {
return
}
}
fi, err := obj.fileInfo(bucket)
if err != nil {
continue
}
objInfo := fi.ToObjectInfo(bucket, obj.name)
action := evalActionFromLifecycle(ctx, *lc, objInfo, false)
switch action {
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
// Skip this entry.
continue
}
select {
case <-ctx.Done():
return
case out <- obj:
}
}
}
func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
// Use ID as the object name...
o.pool = z.getAvailablePoolIdx(ctx, minioMetaBucket, o.ID, 10<<20)
@@ -369,7 +415,7 @@ func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions
filteredResults := o.gatherResults(ctx, outCh)
mc := o.newMetacache()
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(o.Bucket), o: *o}
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix)), o: *o}
// Save listing...
go func() {
+76 -31
View File
@@ -32,6 +32,7 @@ import (
"time"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio/internal/bucket/lifecycle"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/hash"
"github.com/minio/minio/internal/logger"
@@ -64,10 +65,8 @@ type listPathOptions struct {
// Limit the number of results.
Limit int
// The number of disks to ask. Special values:
// 0 uses default number of disks.
// -1 use at least 50% of disks or at least the default number.
AskDisks int
// The number of disks to ask.
AskDisks string
// InclDeleted will keep all entries where latest version is a delete marker.
InclDeleted bool
@@ -96,6 +95,11 @@ type listPathOptions struct {
// pool and set of where the cache is located.
pool, set int
// lcFilter performs filtering based on lifecycle.
// This will filter out objects if the most recent version should be deleted by lifecycle.
// Is not transferred across request calls.
lcFilter *lifecycle.Lifecycle
}
func init() {
@@ -189,7 +193,11 @@ func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCache
}
if resCh != nil {
resErr = io.EOF
resCh <- results
select {
case <-ctx.Done():
// Nobody wants it.
case resCh <- results:
}
}
}()
return func() (metaCacheEntriesSorted, error) {
@@ -351,7 +359,7 @@ func (r *metacacheReader) filter(o listPathOptions) (entries metaCacheEntriesSor
func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOptions) (entries metaCacheEntriesSorted, err error) {
retries := 0
rpc := globalNotificationSys.restClientFromHash(o.Bucket)
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
for {
if contextCanceled(ctx) {
@@ -531,20 +539,39 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
}
}
// getListQuorum interprets list quorum values and returns appropriate
// acceptable quorum expected for list operations
func getListQuorum(quorum string, driveCount int) int {
switch quorum {
case "disk":
// smallest possible value, generally meant for testing.
return 1
case "reduced":
return 2
case "strict":
return -1
}
// Defaults to (driveCount+1)/2 drives per set, defaults to "optimal" value
if driveCount > 0 {
return (driveCount + 1) / 2
} // "3" otherwise.
return 3
}
// Will return io.EOF if continuing would not yield more results.
func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
defer close(results)
o.debugf(color.Green("listPath:")+" with options: %#v", o)
askDisks := o.AskDisks
listingQuorum := o.AskDisks - 1
askDisks := getListQuorum(o.AskDisks, er.setDriveCount)
listingQuorum := askDisks - 1
disks := er.getDisks()
var fallbackDisks []StorageAPI
// Special case: ask all disks if the drive count is 4
if askDisks <= 0 || er.setDriveCount == 4 {
askDisks = len(disks) // with 'strict' quorum list on all online disks.
listingQuorum = len(disks) / 2 // keep this such that we can list all objects with different quorum ratio.
askDisks = len(disks) // with 'strict' quorum list on all drives.
listingQuorum = (len(disks) + 1) / 2 // keep this such that we can list all objects with different quorum ratio.
}
if askDisks > 0 && len(disks) > askDisks {
rand.Shuffle(len(disks), func(i, j int) {
@@ -561,6 +588,13 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
bucket: o.Bucket,
}
// Maximum versions requested for "latest" object
// resolution on versioned buckets, this is to be only
// used when o.Versioned is false
if !o.Versioned {
resolver.requestedVersions = 1
}
ctxDone := ctx.Done()
return listPathRaw(ctx, listPathRawOptions{
disks: disks,
@@ -789,14 +823,30 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
fallback := func(err error) bool {
// Keep track of fallback disks
var fdMu sync.Mutex
fds := opts.fallbackDisks
fallback := func(err error) StorageAPI {
switch err.(type) {
case StorageErr:
// all supported disk errors
// attempt a fallback.
return true
// Attempt to grab a fallback disk
fdMu.Lock()
defer fdMu.Unlock()
if len(fds) == 0 {
return nil
}
fdsCopy := fds
for _, fd := range fdsCopy {
// Grab a fallback disk
fds = fds[1:]
if fd != nil && fd.IsOnline() {
return fd
}
}
}
return false
// Either no more disks for fallback or
// not a storage error.
return nil
}
askDisks := len(disks)
readers := make([]*metacacheReader, askDisks)
@@ -825,25 +875,20 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
}
// fallback only when set.
if len(opts.fallbackDisks) > 0 && fallback(werr) {
for fd := fallback(werr); fd != nil; {
// This fallback is only set when
// askDisks is less than total
// number of disks per set.
for _, fd := range opts.fallbackDisks {
if fd == nil {
continue
}
werr = fd.WalkDir(ctx, WalkDirOptions{
Bucket: opts.bucket,
BaseDir: opts.path,
Recursive: opts.recursive,
ReportNotFound: opts.reportNotFound,
FilterPrefix: opts.filterPrefix,
ForwardTo: opts.forwardTo,
}, w)
if werr == nil {
break
}
werr = fd.WalkDir(ctx, WalkDirOptions{
Bucket: opts.bucket,
BaseDir: opts.path,
Recursive: opts.recursive,
ReportNotFound: opts.reportNotFound,
FilterPrefix: opts.filterPrefix,
ForwardTo: opts.forwardTo,
}, w)
if werr == nil {
break
}
}
w.CloseWithError(werr)
+9 -8
View File
@@ -138,6 +138,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
// Forward some errors?
return nil
}
diskHealthCheckOK(ctx, err)
if len(entries) == 0 {
return nil
}
@@ -179,6 +180,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
s.walkReadMu.Lock()
meta.metadata, err = s.readMetadata(ctx, pathJoin(volumeDir, current, entry))
s.walkReadMu.Unlock()
diskHealthCheckOK(ctx, err)
if err != nil {
logger.LogIf(ctx, err)
continue
@@ -196,6 +198,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
s.walkReadMu.Lock()
meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, current, entry))
s.walkReadMu.Unlock()
diskHealthCheckOK(ctx, err)
if err != nil {
logger.LogIf(ctx, err)
continue
@@ -257,6 +260,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
s.walkReadMu.Lock()
meta.metadata, err = s.readMetadata(ctx, pathJoin(volumeDir, meta.name, xlStorageFormatFile))
s.walkReadMu.Unlock()
diskHealthCheckOK(ctx, err)
switch {
case err == nil:
// It was an object
@@ -266,6 +270,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
out <- meta
case osIsNotExist(err), isSysErrIsDir(err):
meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1))
diskHealthCheckOK(ctx, err)
if err == nil {
// It was an object
out <- meta
@@ -303,16 +308,12 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
return scanDir(opts.BaseDir)
}
func (p *xlStorageDiskIDCheck) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) error {
defer p.updateStorageMetrics(storageMetricWalkDir, opts.Bucket, opts.BaseDir)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writer) (err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricWalkDir, opts.Bucket, opts.BaseDir)
if err != nil {
return err
}
defer done(&err)
return p.storage.WalkDir(ctx, opts, wr)
}
+6 -27
View File
@@ -1651,45 +1651,24 @@ func getClusterTierMetrics() *MetricsGroup {
cacheInterval: 10 * time.Second,
}
mg.RegisterRead(func(ctx context.Context) (metrics []Metric) {
if globalTierConfigMgr.Empty() {
return
}
objLayer := newObjectLayerFn()
if objLayer == nil || globalIsGateway {
return
}
if globalTierConfigMgr.Empty() {
return
}
dui, err := loadDataUsageFromBackend(GlobalContext, objLayer)
if err != nil {
return
}
// data usage has not captured any data yet.
if dui.LastUpdate.IsZero() {
// data usage has not captured any tier stats yet.
if dui.TierStats == nil {
return
}
// e.g minio_cluster_ilm_transitioned_bytes{tier="S3TIER-1"}=136314880
// minio_cluster_ilm_transitioned_objects{tier="S3TIER-1"}=1
// minio_cluster_ilm_transitioned_versions{tier="S3TIER-1"}=3
for tier, st := range dui.TierStats.Tiers {
metrics = append(metrics, Metric{
Description: getClusterTransitionedBytesMD(),
Value: float64(st.TotalSize),
VariableLabels: map[string]string{"tier": tier},
})
metrics = append(metrics, Metric{
Description: getClusterTransitionedObjectsMD(),
Value: float64(st.NumObjects),
VariableLabels: map[string]string{"tier": tier},
})
metrics = append(metrics, Metric{
Description: getClusterTransitionedVersionsMD(),
Value: float64(st.NumVersions),
VariableLabels: map[string]string{"tier": tier},
})
}
return metrics
return dui.tierMetrics()
})
return mg
}
+1 -1
View File
@@ -674,7 +674,7 @@ func metricsHandler() http.Handler {
// AuthMiddleware checks if the bearer token is valid and authorized.
func AuthMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, groups, owner, authErr := webRequestAuthenticate(r)
claims, groups, owner, authErr := metricsRequestAuthenticate(r)
if authErr != nil || !claims.VerifyIssuer("prometheus", true) {
w.WriteHeader(http.StatusForbidden)
return
+17
View File
@@ -170,6 +170,10 @@ func (di *distLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeout
}) {
timeout.LogFailure()
cancel()
switch err := newCtx.Err(); err {
case context.Canceled:
return LockContext{ctx: ctx, cancel: func() {}}, err
}
return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
}
timeout.LogSuccess(UTCNow().Sub(start))
@@ -195,6 +199,10 @@ func (di *distLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeou
}) {
timeout.LogFailure()
cancel()
switch err := newCtx.Err(); err {
case context.Canceled:
return LockContext{ctx: ctx, cancel: func() {}}, err
}
return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
}
timeout.LogSuccess(UTCNow().Sub(start))
@@ -225,6 +233,7 @@ func (n *nsLockMap) NewNSLock(lockers func() ([]dsync.NetLocker, string), volume
if n.isDistErasure {
drwmutex := dsync.NewDRWMutex(&dsync.Dsync{
GetLockers: lockers,
Timeouts: dsync.DefaultTimeouts,
}, pathsJoinPrefix(volume, paths...)...)
return &distLockInstance{drwmutex, opsID}
}
@@ -246,6 +255,10 @@ func (li *localLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeou
li.ns.unlock(li.volume, li.paths[si], readLock)
}
}
switch err := ctx.Err(); err {
case context.Canceled:
return LockContext{}, err
}
return LockContext{}, OperationTimedOut{}
}
success[i] = 1
@@ -279,6 +292,10 @@ func (li *localLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeo
li.ns.unlock(li.volume, li.paths[si], readLock)
}
}
switch err := ctx.Err(); err {
case context.Canceled:
return LockContext{}, err
}
return LockContext{}, OperationTimedOut{}
}
success[i] = 1
+52
View File
@@ -1553,6 +1553,58 @@ func (sys *NotificationSys) ServiceFreeze(ctx context.Context, freeze bool) []No
return nerrs
}
// Netperf - perform mesh style network throughput test
func (sys *NotificationSys) Netperf(ctx context.Context, duration time.Duration) []madmin.NetperfNodeResult {
length := len(sys.allPeerClients)
if length == 0 {
// For single node erasure setup.
return nil
}
results := make([]madmin.NetperfNodeResult, length)
scheme := "http"
if globalIsTLS {
scheme = "https"
}
var wg sync.WaitGroup
for index := range sys.peerClients {
if sys.peerClients[index] == nil {
continue
}
wg.Add(1)
go func(index int) {
defer wg.Done()
r, err := sys.peerClients[index].Netperf(ctx, duration)
u := &url.URL{
Scheme: scheme,
Host: sys.peerClients[index].host.String(),
}
if err != nil {
results[index].Error = err.Error()
} else {
results[index] = r
}
results[index].Endpoint = u.String()
}(index)
}
wg.Add(1)
go func() {
defer wg.Done()
r := netperf(ctx, duration)
u := &url.URL{
Scheme: scheme,
Host: globalLocalNodeName,
}
results[len(results)-1] = r
results[len(results)-1].Endpoint = u.String()
}()
wg.Wait()
return results
}
// Speedtest run GET/PUT tests at input concurrency for requested object size,
// optionally you can extend the tests longer with time.Duration.
func (sys *NotificationSys) Speedtest(ctx context.Context, size int,
+8
View File
@@ -31,6 +31,9 @@ func toObjectErr(err error, params ...string) error {
if err == nil {
return nil
}
if errors.Is(err, context.Canceled) {
return context.Canceled
}
switch err.Error() {
case errVolumeNotFound.Error():
apiErr := BucketNotFound{}
@@ -698,3 +701,8 @@ func isErrMethodNotAllowed(err error) bool {
var methodNotAllowed MethodNotAllowed
return errors.As(err, &methodNotAllowed)
}
func isErrInvalidRange(err error) bool {
_, ok := err.(InvalidRange)
return ok
}
+3 -1
View File
@@ -66,7 +66,9 @@ type ObjectOptions struct {
ReplicationSourceTaggingTimestamp time.Time // set if MinIOSourceTaggingTimestamp received
ReplicationSourceLegalholdTimestamp time.Time // set if MinIOSourceObjectLegalholdTimestamp received
ReplicationSourceRetentionTimestamp time.Time // set if MinIOSourceObjectRetentionTimestamp received
DeletePrefix bool // set true to enforce a prefix deletion, only application for DeleteObject API,
DeletePrefix bool // set true to enforce a prefix deletion, only application for DeleteObject API,
Speedtest bool // object call specifically meant for SpeedTest code, set to 'true' when invoked by SpeedtestHandler.
// Use the maximum parity (N/2), used when saving server configuration files
MaxParity bool
+111 -99
View File
@@ -206,6 +206,9 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
Start: offset,
End: offset + length,
}
if length == -1 {
rs.End = -1
}
return getObjectNInfo(ctx, bucket, object, rs, r.Header, readLock, opts)
}
@@ -412,20 +415,29 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
if err != nil {
var (
reader *GetObjectReader
proxy bool
proxy proxyResult
perr error
)
proxytgts := getproxyTargets(ctx, bucket, object, opts)
if !proxytgts.Empty() {
// proxy to replication target if active-active replication is in place.
reader, proxy = proxyGetToReplicationTarget(ctx, bucket, object, rs, r.Header, opts, proxytgts)
if reader != nil && proxy {
reader, proxy, perr = proxyGetToReplicationTarget(ctx, bucket, object, rs, r.Header, opts, proxytgts)
if perr != nil && !isErrObjectNotFound(ErrorRespToObjectError(perr, bucket, object)) &&
!isErrVersionNotFound(ErrorRespToObjectError(perr, bucket, object)) {
logger.LogIf(ctx, fmt.Errorf("Replication proxy failed for %s/%s(%s) - %w", bucket, object, opts.VersionID, perr))
}
if reader != nil && proxy.Proxy && perr == nil {
gr = reader
}
}
if reader == nil || !proxy {
if reader == nil || !proxy.Proxy {
if isErrPreconditionFailed(err) {
return
}
if proxy.Err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, proxy.Err), r.URL)
return
}
if globalBucketVersioningSys.Enabled(bucket) && gr != nil {
if !gr.ObjInfo.VersionPurgeStatus.Empty() {
// Shows the replication status of a permanent delete of a version
@@ -631,22 +643,28 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
return
}
// Get request range.
var rs *HTTPRangeSpec
rangeHeader := r.Header.Get(xhttp.Range)
objInfo, err := getObjectInfo(ctx, bucket, object, opts)
if err != nil {
var (
proxy bool
proxy proxyResult
oi ObjectInfo
)
// proxy HEAD to replication target if active-active replication configured on bucket
proxytgts := getproxyTargets(ctx, bucket, object, opts)
if !proxytgts.Empty() {
oi, proxy = proxyHeadToReplicationTarget(ctx, bucket, object, opts, proxytgts)
if proxy {
if rangeHeader != "" {
rs, _ = parseRequestRangeSpec(rangeHeader)
}
oi, proxy = proxyHeadToReplicationTarget(ctx, bucket, object, rs, opts, proxytgts)
if proxy.Proxy {
objInfo = oi
}
}
if !proxy {
if !proxy.Proxy {
if globalBucketVersioningSys.Enabled(bucket) {
switch {
case !objInfo.VersionPurgeStatus.Empty():
@@ -703,9 +721,6 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
return
}
// Get request range.
var rs *HTTPRangeSpec
rangeHeader := r.Header.Get(xhttp.Range)
if rangeHeader != "" {
// Both 'Range' and 'partNumber' cannot be specified at the same time
if opts.PartNumber > 0 {
@@ -2889,12 +2904,20 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
}
etag := partInfo.ETag
switch kind, encrypted := crypto.IsEncrypted(mi.UserDefined); {
case encrypted:
if kind, encrypted := crypto.IsEncrypted(mi.UserDefined); encrypted {
switch kind {
case crypto.S3KMS:
w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
w.Header().Set(xhttp.AmzServerSideEncryptionKmsID, mi.KMSKeyID())
if kmsCtx, ok := mi.UserDefined[crypto.MetaContext]; ok {
w.Header().Set(xhttp.AmzServerSideEncryptionKmsContext, kmsCtx)
}
if len(etag) >= 32 && strings.Count(etag, "-") != 1 {
etag = etag[len(etag)-32:]
}
case crypto.S3:
w.Header().Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
etag = tryDecryptETag(objectEncryptionKey[:], etag, false)
etag, _ = DecryptETag(objectEncryptionKey, ObjectInfo{ETag: etag})
case crypto.SSEC:
w.Header().Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerAlgorithm))
w.Header().Set(xhttp.AmzServerSideEncryptionCustomerKeyMD5, r.Header.Get(xhttp.AmzServerSideEncryptionCustomerKeyMD5))
@@ -3002,34 +3025,29 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
return
}
var ssec bool
if _, ok := crypto.IsEncrypted(listPartsInfo.UserDefined); ok && objectAPI.IsEncryptionSupported() {
var key []byte
if crypto.SSEC.IsEncrypted(listPartsInfo.UserDefined) {
ssec = true
}
// We have to adjust the size of encrypted parts since encrypted parts
// are slightly larger due to encryption overhead.
// Further, we have to adjust the ETags of parts when using SSE-S3.
// Due to AWS S3, SSE-S3 encrypted parts return the plaintext ETag
// being the content MD5 of that particular part. This is not the
// case for SSE-C and SSE-KMS objects.
if kind, ok := crypto.IsEncrypted(listPartsInfo.UserDefined); ok && objectAPI.IsEncryptionSupported() {
var objectEncryptionKey []byte
if crypto.S3.IsEncrypted(listPartsInfo.UserDefined) {
// Calculating object encryption key
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, listPartsInfo.UserDefined)
if kind == crypto.S3 {
objectEncryptionKey, err = decryptObjectInfo(nil, bucket, object, listPartsInfo.UserDefined)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
for i := range listPartsInfo.Parts {
curp := listPartsInfo.Parts[i]
curp.ETag = tryDecryptETag(objectEncryptionKey, curp.ETag, ssec)
if !ssec {
var partSize uint64
partSize, err = sio.DecryptedSize(uint64(curp.Size))
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
curp.Size = int64(partSize)
for i, p := range listPartsInfo.Parts {
listPartsInfo.Parts[i].ETag = tryDecryptETag(objectEncryptionKey, p.ETag, kind != crypto.S3)
size, err := sio.DecryptedSize(uint64(p.Size))
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
listPartsInfo.Parts[i] = curp
listPartsInfo.Parts[i].Size = int64(size)
}
}
@@ -3161,66 +3179,6 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
return
}
var objectEncryptionKey []byte
var isEncrypted, ssec bool
if objectAPI.IsEncryptionSupported() {
mi, err := objectAPI.GetMultipartInfo(ctx, bucket, object, uploadID, ObjectOptions{})
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if _, ok := crypto.IsEncrypted(mi.UserDefined); ok {
var key []byte
isEncrypted = true
ssec = crypto.SSEC.IsEncrypted(mi.UserDefined)
if crypto.S3.IsEncrypted(mi.UserDefined) {
// Calculating object encryption key
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, mi.UserDefined)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
}
}
}
partsMap := make(map[string]PartInfo)
if isEncrypted {
maxParts := 10000
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, maxParts, ObjectOptions{})
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
for _, part := range listPartsInfo.Parts {
partsMap[strconv.Itoa(part.PartNumber)] = part
}
}
// Complete parts.
completeParts := make([]CompletePart, 0, len(complMultipartUpload.Parts))
originalCompleteParts := make([]CompletePart, 0, len(complMultipartUpload.Parts))
for _, part := range complMultipartUpload.Parts {
part.ETag = canonicalizeETag(part.ETag)
originalCompleteParts = append(originalCompleteParts, part)
if isEncrypted {
// ETag is stored in the backend in encrypted form. Validate client sent ETag with
// decrypted ETag.
if bkPartInfo, ok := partsMap[strconv.Itoa(part.PartNumber)]; ok {
bkETag := tryDecryptETag(objectEncryptionKey, bkPartInfo.ETag, ssec)
if bkETag != part.ETag {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPart), r.URL)
return
}
part.ETag = bkPartInfo.ETag
}
}
completeParts = append(completeParts, part)
}
// Calculate s3 compatible md5sum for complete multipart.
s3MD5 := getCompleteMultipartMD5(originalCompleteParts)
completeMultiPartUpload := objectAPI.CompleteMultipartUpload
if api.CacheAPI() != nil {
completeMultiPartUpload = api.CacheAPI().CompleteMultipartUpload
@@ -3263,14 +3221,68 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
return
}
// preserve ETag if set, or set from parts.
if _, ok := opts.UserDefined["etag"]; !ok {
opts.UserDefined["etag"] = s3MD5
// First, we compute the ETag of the multipart object.
// The ETag of a multi-part object is always:
// ETag := MD5(ETag_p1, ETag_p2, ...)+"-N" (N being the number of parts)
//
// This is independent of encryption. An encrypted multipart
// object also has an ETag that is the MD5 of its part ETags.
// The fact the in case of encryption the ETag of a part is
// not the MD5 of the part content does not change that.
var completeETags []etag.ETag
for _, part := range complMultipartUpload.Parts {
ETag, err := etag.Parse(part.ETag)
if err != nil {
continue
}
completeETags = append(completeETags, ETag)
}
multipartETag := etag.Multipart(completeETags...)
opts.UserDefined["etag"] = multipartETag.String()
// However, in case of encryption, the persisted part ETags don't match
// what we have sent to the client during PutObjectPart. The reason is
// that ETags are encrypted. Hence, the client will send a list of complete
// part ETags of which non can match the ETag of any part. For example
// ETag (client): 30902184f4e62dd8f98f0aaff810c626
// ETag (server-internal): 20000f00ce5dc16e3f3b124f586ae1d88e9caa1c598415c2759bbb50e84a59f630902184f4e62dd8f98f0aaff810c626
//
// Therefore, we adjust all ETags sent by the client to match what is stored
// on the backend.
if objectAPI.IsEncryptionSupported() {
mi, err := objectAPI.GetMultipartInfo(ctx, bucket, object, uploadID, ObjectOptions{})
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if _, ok := crypto.IsEncrypted(mi.UserDefined); ok {
const MaxParts = 10000
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, MaxParts, ObjectOptions{})
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
sort.Slice(listPartsInfo.Parts, func(i, j int) bool {
return listPartsInfo.Parts[i].PartNumber < listPartsInfo.Parts[j].PartNumber
})
sort.Slice(complMultipartUpload.Parts, func(i, j int) bool {
return complMultipartUpload.Parts[i].PartNumber < complMultipartUpload.Parts[j].PartNumber
})
for i := range listPartsInfo.Parts {
for j := range complMultipartUpload.Parts {
if listPartsInfo.Parts[i].PartNumber == complMultipartUpload.Parts[j].PartNumber {
complMultipartUpload.Parts[j].ETag = listPartsInfo.Parts[i].ETag
continue
}
}
}
}
}
w = &whiteSpaceWriter{ResponseWriter: w, Flusher: w.(http.Flusher)}
completeDoneCh := sendWhiteSpace(w)
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, completeParts, opts)
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, complMultipartUpload.Parts, opts)
// Stop writing white spaces to the client. Note that close(doneCh) style is not used as it
// can cause white space to be written after we send XML response in a race condition.
headerWritten := <-completeDoneCh
+24
View File
@@ -1119,3 +1119,27 @@ func (client *peerRESTClient) GetLastDayTierStats(ctx context.Context) (dailyAll
}
return dailyAllTierStats(result), nil
}
// DevNull - Used by netperf to pump data to peer
func (client *peerRESTClient) DevNull(ctx context.Context, r io.Reader) error {
respBody, err := client.callWithContext(ctx, peerRESTMethodDevNull, nil, r, -1)
if err != nil {
return err
}
defer http.DrainBody(respBody)
return err
}
// Netperf - To initiate netperf on peer
func (client *peerRESTClient) Netperf(ctx context.Context, duration time.Duration) (madmin.NetperfNodeResult, error) {
var result madmin.NetperfNodeResult
values := make(url.Values)
values.Set(peerRESTDuration, duration.String())
respBody, err := client.callWithContext(context.Background(), peerRESTMethodNetperf, values, nil, -1)
if err != nil {
return result, err
}
defer http.DrainBody(respBody)
err = gob.NewDecoder(respBody).Decode(&result)
return result, err
}
+3 -1
View File
@@ -18,7 +18,7 @@
package cmd
const (
peerRESTVersion = "v20" // Add drivespeedtest
peerRESTVersion = "v21" // Add netperf
peerRESTVersionPrefix = SlashSeparator + peerRESTVersion
peerRESTPrefix = minioReservedBucketPath + "/peer"
peerRESTPath = peerRESTPrefix + peerRESTVersionPrefix
@@ -70,6 +70,8 @@ const (
peerRESTMethodReloadSiteReplicationConfig = "/reloadsitereplicationconfig"
peerRESTMethodReloadPoolMeta = "/reloadpoolmeta"
peerRESTMethodGetLastDayTierStats = "/getlastdaytierstats"
peerRESTMethodDevNull = "/devnull"
peerRESTMethodNetperf = "/netperf"
)
const (
+46 -145
View File
@@ -27,20 +27,15 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/dustin/go-humanize"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/minio/madmin-go"
b "github.com/minio/minio/internal/bucket/bandwidth"
"github.com/minio/minio/internal/event"
"github.com/minio/minio/internal/hash"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/randreader"
"github.com/tinylib/msgp/msgp"
)
@@ -1143,146 +1138,6 @@ func (s *peerRESTServer) GetPeerMetrics(w http.ResponseWriter, r *http.Request)
}
}
// SpeedtestResult return value of the speedtest function
type SpeedtestResult struct {
Endpoint string
Uploads uint64
Downloads uint64
Error string
}
func newRandomReader(size int) io.Reader {
return io.LimitReader(randreader.New(), int64(size))
}
// Runs the speedtest on local MinIO process.
func selfSpeedtest(ctx context.Context, size, concurrent int, duration time.Duration, storageClass string) (SpeedtestResult, error) {
objAPI := newObjectLayerFn()
if objAPI == nil {
return SpeedtestResult{}, errServerNotInitialized
}
var errOnce sync.Once
var retError string
var wg sync.WaitGroup
var totalBytesWritten uint64
var totalBytesRead uint64
objCountPerThread := make([]uint64, concurrent)
uploadsCtx, uploadsCancel := context.WithCancel(context.Background())
defer uploadsCancel()
go func() {
time.Sleep(duration)
uploadsCancel()
}()
objNamePrefix := "speedtest/objects/" + uuid.New().String()
wg.Add(concurrent)
for i := 0; i < concurrent; i++ {
go func(i int) {
defer wg.Done()
for {
hashReader, err := hash.NewReader(newRandomReader(size),
int64(size), "", "", int64(size))
if err != nil {
if !contextCanceled(uploadsCtx) {
errOnce.Do(func() {
retError = err.Error()
logger.LogIf(ctx, err)
})
}
uploadsCancel()
return
}
reader := NewPutObjReader(hashReader)
objInfo, err := objAPI.PutObject(uploadsCtx, minioMetaBucket, fmt.Sprintf("%s.%d.%d",
objNamePrefix, i, objCountPerThread[i]), reader, ObjectOptions{
UserDefined: map[string]string{
xhttp.AmzStorageClass: storageClass,
},
})
if err != nil {
if !contextCanceled(uploadsCtx) {
errOnce.Do(func() {
retError = err.Error()
logger.LogIf(ctx, err)
})
}
uploadsCancel()
return
}
atomic.AddUint64(&totalBytesWritten, uint64(objInfo.Size))
objCountPerThread[i]++
}
}(i)
}
wg.Wait()
// We already saw write failures, no need to proceed into read's
if retError != "" {
return SpeedtestResult{Uploads: totalBytesWritten, Downloads: totalBytesRead, Error: retError}, nil
}
downloadsCtx, downloadsCancel := context.WithCancel(context.Background())
defer downloadsCancel()
go func() {
time.Sleep(duration)
downloadsCancel()
}()
wg.Add(concurrent)
for i := 0; i < concurrent; i++ {
go func(i int) {
defer wg.Done()
var j uint64
if objCountPerThread[i] == 0 {
return
}
for {
if objCountPerThread[i] == j {
j = 0
}
r, err := objAPI.GetObjectNInfo(downloadsCtx, minioMetaBucket, fmt.Sprintf("%s.%d.%d",
objNamePrefix, i, j), nil, nil, noLock, ObjectOptions{})
if err != nil {
if !contextCanceled(downloadsCtx) {
errOnce.Do(func() {
retError = err.Error()
logger.LogIf(ctx, err)
})
}
downloadsCancel()
return
}
n, err := io.Copy(ioutil.Discard, r)
r.Close()
if err == nil {
// Only capture success criteria - do not
// have to capture failed reads, truncated
// reads etc.
atomic.AddUint64(&totalBytesRead, uint64(n))
}
if err != nil {
if !contextCanceled(downloadsCtx) {
errOnce.Do(func() {
retError = err.Error()
logger.LogIf(ctx, err)
})
}
downloadsCancel()
return
}
j++
}
}(i)
}
wg.Wait()
return SpeedtestResult{Uploads: totalBytesWritten, Downloads: totalBytesRead, Error: retError}, nil
}
func (s *peerRESTServer) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
s.writeErrorResponse(w, errors.New("invalid request"))
@@ -1382,6 +1237,50 @@ func (s *peerRESTServer) DriveSpeedTestHandler(w http.ResponseWriter, r *http.Re
logger.LogIf(r.Context(), gob.NewEncoder(w).Encode(result))
}
// DevNull - everything goes to io.Discard
func (s *peerRESTServer) DevNull(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
s.writeErrorResponse(w, errors.New("invalid request"))
return
}
globalNetPerfRX.Connect()
defer globalNetPerfRX.Disconnect()
connectTime := time.Now()
ctx := newContext(r, w, "DevNull")
for {
n, err := io.CopyN(io.Discard, r.Body, 128*humanize.KiByte)
atomic.AddUint64(&globalNetPerfRX.RX, uint64(n))
if err != nil && err != io.EOF {
// If there is a disconnection before globalNetPerfMinDuration (we give a margin of error of 1 sec)
// would mean the network is not stable. Logging here will help in debugging network issues.
if time.Since(connectTime) < (globalNetPerfMinDuration - time.Second) {
logger.LogIf(ctx, err)
}
}
if err != nil {
break
}
}
}
// Netperf - perform netperf
func (s *peerRESTServer) Netperf(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
s.writeErrorResponse(w, errors.New("invalid request"))
return
}
durationStr := r.Form.Get(peerRESTDuration)
duration, err := time.ParseDuration(durationStr)
if err != nil || duration.Seconds() == 0 {
duration = time.Second * 10
}
result := netperf(r.Context(), duration.Round(time.Second))
logger.LogIf(r.Context(), gob.NewEncoder(w).Encode(result))
}
// registerPeerRESTHandlers - register peer rest router.
func registerPeerRESTHandlers(router *mux.Router) {
server := &peerRESTServer{}
@@ -1429,6 +1328,8 @@ func registerPeerRESTHandlers(router *mux.Router) {
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadTransitionTierConfig).HandlerFunc(httpTraceHdrs(server.LoadTransitionTierConfigHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSpeedtest).HandlerFunc(httpTraceHdrs(server.SpeedtestHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDriveSpeedTest).HandlerFunc(httpTraceHdrs(server.DriveSpeedTestHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodNetperf).HandlerFunc(httpTraceHdrs(server.Netperf))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDevNull).HandlerFunc(httpTraceHdrs(server.DevNull))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodReloadSiteReplicationConfig).HandlerFunc(httpTraceHdrs(server.ReloadSiteReplicationConfigHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodReloadPoolMeta).HandlerFunc(httpTraceHdrs(server.ReloadPoolMetaHandler))
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodGetLastDayTierStats).HandlerFunc(httpTraceHdrs(server.GetLastDayTierStatsHandler))
+295
View File
@@ -0,0 +1,295 @@
// Copyright (c) 2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"sync"
"sync/atomic"
"time"
"github.com/dustin/go-humanize"
"github.com/google/uuid"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/hash"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/pkg/randreader"
)
// SpeedtestResult return value of the speedtest function
type SpeedtestResult struct {
Endpoint string
Uploads uint64
Downloads uint64
Error string
}
func newRandomReader(size int) io.Reader {
return io.LimitReader(randreader.New(), int64(size))
}
// Runs the speedtest on local MinIO process.
func selfSpeedtest(ctx context.Context, size, concurrent int, duration time.Duration, storageClass string) (SpeedtestResult, error) {
objAPI := newObjectLayerFn()
if objAPI == nil {
return SpeedtestResult{}, errServerNotInitialized
}
var errOnce sync.Once
var retError string
var wg sync.WaitGroup
var totalBytesWritten uint64
var totalBytesRead uint64
objCountPerThread := make([]uint64, concurrent)
uploadsCtx, uploadsCancel := context.WithCancel(context.Background())
defer uploadsCancel()
go func() {
time.Sleep(duration)
uploadsCancel()
}()
objNamePrefix := "speedtest/objects/" + uuid.New().String()
wg.Add(concurrent)
for i := 0; i < concurrent; i++ {
go func(i int) {
defer wg.Done()
for {
hashReader, err := hash.NewReader(newRandomReader(size),
int64(size), "", "", int64(size))
if err != nil {
if !contextCanceled(uploadsCtx) && !errors.Is(err, context.Canceled) {
errOnce.Do(func() {
retError = err.Error()
})
}
uploadsCancel()
return
}
reader := NewPutObjReader(hashReader)
objInfo, err := objAPI.PutObject(uploadsCtx, minioMetaBucket, fmt.Sprintf("%s.%d.%d",
objNamePrefix, i, objCountPerThread[i]), reader, ObjectOptions{
UserDefined: map[string]string{
xhttp.AmzStorageClass: storageClass,
},
Speedtest: true,
})
if err != nil {
objCountPerThread[i]--
if !contextCanceled(uploadsCtx) && !errors.Is(err, context.Canceled) {
errOnce.Do(func() {
retError = err.Error()
})
}
uploadsCancel()
return
}
atomic.AddUint64(&totalBytesWritten, uint64(objInfo.Size))
objCountPerThread[i]++
}
}(i)
}
wg.Wait()
// We already saw write failures, no need to proceed into read's
if retError != "" {
return SpeedtestResult{Uploads: totalBytesWritten, Downloads: totalBytesRead, Error: retError}, nil
}
downloadsCtx, downloadsCancel := context.WithCancel(context.Background())
defer downloadsCancel()
go func() {
time.Sleep(duration)
downloadsCancel()
}()
wg.Add(concurrent)
for i := 0; i < concurrent; i++ {
go func(i int) {
defer wg.Done()
var j uint64
if objCountPerThread[i] == 0 {
return
}
for {
if objCountPerThread[i] == j {
j = 0
}
r, err := objAPI.GetObjectNInfo(downloadsCtx, minioMetaBucket, fmt.Sprintf("%s.%d.%d",
objNamePrefix, i, j), nil, nil, noLock, ObjectOptions{})
if err != nil {
if isErrObjectNotFound(err) {
continue
}
if !contextCanceled(downloadsCtx) && !errors.Is(err, context.Canceled) {
errOnce.Do(func() {
retError = err.Error()
})
}
downloadsCancel()
return
}
n, err := io.Copy(ioutil.Discard, r)
r.Close()
if err == nil {
// Only capture success criteria - do not
// have to capture failed reads, truncated
// reads etc.
atomic.AddUint64(&totalBytesRead, uint64(n))
}
if err != nil {
if !contextCanceled(downloadsCtx) && !errors.Is(err, context.Canceled) {
errOnce.Do(func() {
retError = err.Error()
})
}
downloadsCancel()
return
}
j++
}
}(i)
}
wg.Wait()
return SpeedtestResult{Uploads: totalBytesWritten, Downloads: totalBytesRead, Error: retError}, nil
}
// To collect RX stats during "mc support perf net"
// RXSample holds the RX bytes for the duration between
// the last peer to connect and the first peer to disconnect.
// This is to improve the RX throughput accuracy.
type netPerfRX struct {
RX uint64 // RX bytes
lastToConnect time.Time // time at which last peer to connect to us
firstToDisconnect time.Time // time at which the first peer disconnects from us
RXSample uint64 // RX bytes between lastToConnect and firstToDisconnect
activeConnections uint64
sync.RWMutex
}
func (n *netPerfRX) Connect() {
n.Lock()
defer n.Unlock()
n.activeConnections++
atomic.StoreUint64(&globalNetPerfRX.RX, 0)
n.lastToConnect = time.Now()
}
func (n *netPerfRX) Disconnect() {
n.Lock()
defer n.Unlock()
n.activeConnections--
if n.firstToDisconnect.IsZero() {
n.RXSample = atomic.LoadUint64(&n.RX)
n.firstToDisconnect = time.Now()
}
}
func (n *netPerfRX) ActiveConnections() uint64 {
n.RLock()
defer n.RUnlock()
return n.activeConnections
}
func (n *netPerfRX) Reset() {
n.RLock()
defer n.RUnlock()
n.RX = 0
n.RXSample = 0
n.lastToConnect = time.Time{}
n.firstToDisconnect = time.Time{}
}
// Reader to read random data.
type netperfReader struct {
n uint64
eof chan struct{}
buf []byte
}
func (m *netperfReader) Read(b []byte) (int, error) {
select {
case <-m.eof:
return 0, io.EOF
default:
}
n := copy(b, m.buf)
atomic.AddUint64(&m.n, uint64(n))
return n, nil
}
func netperf(ctx context.Context, duration time.Duration) madmin.NetperfNodeResult {
r := &netperfReader{eof: make(chan struct{})}
r.buf = make([]byte, 128*humanize.KiByte)
rand.Read(r.buf)
connectionsPerPeer := 16
if len(globalNotificationSys.peerClients) > 16 {
// For a large cluster it's enough to have 1 connection per peer to saturate the network.
connectionsPerPeer = 1
}
errStr := ""
var wg sync.WaitGroup
for index := range globalNotificationSys.peerClients {
if globalNotificationSys.peerClients[index] == nil {
continue
}
go func(index int) {
for i := 0; i < connectionsPerPeer; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := globalNotificationSys.peerClients[index].DevNull(ctx, r)
if err != nil {
errStr = err.Error()
}
}()
}
}(index)
}
time.Sleep(duration)
close(r.eof)
wg.Wait()
for {
if globalNetPerfRX.ActiveConnections() == 0 {
break
}
time.Sleep(time.Second)
}
rx := float64(globalNetPerfRX.RXSample)
delta := globalNetPerfRX.firstToDisconnect.Sub(globalNetPerfRX.lastToConnect)
if delta < 0 {
rx = 0
errStr = "network disconnection issues detected"
}
globalNetPerfRX.Reset()
return madmin.NetperfNodeResult{Endpoint: "", TX: r.n / uint64(duration.Seconds()), RX: uint64(rx / delta.Seconds()), Error: errStr}
}
+1 -24
View File
@@ -19,7 +19,6 @@ package cmd
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
@@ -121,31 +120,9 @@ func isServerResolvable(endpoint Endpoint, timeout time.Duration) error {
Path: pathJoin(healthCheckPathPrefix, healthCheckLivenessPath),
}
var tlsConfig *tls.Config
if globalIsTLS {
tlsConfig = &tls.Config{
RootCAs: globalRootCAs,
}
}
httpClient := &http.Client{
Transport:
// For more details about various values used here refer
// https://golang.org/pkg/net/http/#Transport documentation
&http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: xhttp.NewCustomDialContext(3 * time.Second),
ResponseHeaderTimeout: 3 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ExpectContinueTimeout: 3 * time.Second,
TLSClientConfig: tlsConfig,
// Go net/http automatically unzip if content-type is
// gzip disable this feature, as we are always interested
// in raw stream.
DisableCompression: true,
},
Transport: globalInternodeTransport,
}
defer httpClient.CloseIdleConnections()
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
+21 -6
View File
@@ -28,6 +28,7 @@ import (
"math/rand"
"os"
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
@@ -299,6 +300,8 @@ func configRetriableErrors(err error) bool {
}
func initServer(ctx context.Context, newObject ObjectLayer) error {
t1 := time.Now()
// Once the config is fully loaded, initialize the new object layer.
setObjectLayer(newObject)
@@ -351,7 +354,7 @@ func initServer(ctx context.Context, newObject ObjectLayer) error {
// All successful return.
if globalIsDistErasure {
// These messages only meant primarily for distributed setup, so only log during distributed setup.
logger.Info("All MinIO sub-systems initialized successfully")
logger.Info("All MinIO sub-systems initialized successfully in %s", time.Since(t1))
}
return nil
}
@@ -401,7 +404,7 @@ func serverMain(ctx *cli.Context) {
// Initialize globalConsoleSys system
globalConsoleSys = NewConsoleLogger(GlobalContext)
logger.AddTarget(globalConsoleSys)
logger.AddSystemTarget(globalConsoleSys)
// Perform any self-tests
bitrotSelfTest()
@@ -448,6 +451,17 @@ func serverMain(ctx *cli.Context) {
// Set system resources to maximum.
setMaxResources()
// Verify kernel release and version.
if oldLinux() {
logger.Info(color.RedBold("WARNING: Detected Linux kernel version older than 4.0.0 release, there are some known potential performance problems with this kernel version. MinIO recommends a minimum of 4.x.x linux kernel version for best performance"))
}
maxProcs := runtime.GOMAXPROCS(0)
cpuProcs := runtime.NumCPU()
if maxProcs < cpuProcs {
logger.Info(color.RedBold("WARNING: Detected GOMAXPROCS(%d) < NumCPU(%d), please make sure to provide all PROCS to MinIO for optimal performance", maxProcs, cpuProcs))
}
// Configure server.
handler, err := configureServerHandler(globalEndpoints)
if err != nil {
@@ -493,7 +507,8 @@ func serverMain(ctx *cli.Context) {
logFatalErrs(err, Endpoint{}, true)
}
logger.SetDeploymentID(globalDeploymentID)
xhttp.SetDeploymentID(globalDeploymentID)
xhttp.SetMinIOVersion(Version)
// Enable background operations for erasure coding
if globalIsErasure {
@@ -506,11 +521,11 @@ func serverMain(ctx *cli.Context) {
if globalActiveCred.Equal(auth.DefaultCredentials) {
msg := fmt.Sprintf("WARNING: Detected default credentials '%s', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables",
globalActiveCred)
logStartupMessage(color.RedBold(msg))
logger.Info(color.RedBold(msg))
}
if !globalCLIContext.StrictS3Compat {
logStartupMessage(color.RedBold("WARNING: Strict AWS S3 compatible incoming PUT, POST content payload validation is turned off, caution is advised do not use in production"))
logger.Info(color.RedBold("WARNING: Strict AWS S3 compatible incoming PUT, POST content payload validation is turned off, caution is advised do not use in production"))
}
if err = initServer(GlobalContext, newObject); err != nil {
@@ -594,7 +609,7 @@ func serverMain(ctx *cli.Context) {
// initialize the new disk cache objects.
if globalCacheConfig.Enabled {
logStartupMessage(color.Yellow("WARNING: Disk caching is deprecated for single/multi drive MinIO setups. Please migrate to using MinIO S3 gateway instead of disk caching"))
logger.Info(color.Yellow("WARNING: Disk caching is deprecated for single/multi drive MinIO setups. Please migrate to using MinIO S3 gateway instead of disk caching"))
var cacheAPI CacheObjectLayer
cacheAPI, err = newServerCacheObjects(GlobalContext, globalCacheConfig)
logger.FatalIf(err, "Unable to initialize disk caching")
+20 -1
View File
@@ -21,10 +21,28 @@ import (
"runtime"
"runtime/debug"
"github.com/minio/minio/internal/kernel"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/sys"
)
func oldLinux() bool {
currentKernel, err := kernel.CurrentVersion()
if err != nil {
// Could not probe the kernel version
return false
}
if currentKernel == 0 {
// We could not get any valid value return false
return false
}
// legacy linux indicator for printing warnings
// about older Linux kernels and Go runtime.
return currentKernel < kernel.Version(4, 0, 0)
}
func setMaxResources() (err error) {
// Set the Go runtime max threads threshold to 90% of kernel setting.
sysMaxThreads, mErr := sys.GetMaxThreads()
@@ -44,7 +62,8 @@ func setMaxResources() (err error) {
}
if maxLimit < 4096 && runtime.GOOS != globalWindowsOSName {
logger.Info("WARNING: maximum file descriptor limit %d is too low for production servers. At least 4096 is recommended. Fix with \"ulimit -n 4096\"", maxLimit)
logger.Info("WARNING: maximum file descriptor limit %d is too low for production servers. At least 4096 is recommended. Fix with \"ulimit -n 4096\"",
maxLimit)
}
if err = sys.SetMaxOpenFileLimit(maxLimit, maxLimit); err != nil {
+20 -23
View File
@@ -45,9 +45,9 @@ func mustGetStorageInfo(objAPI ObjectLayer) StorageInfo {
// Prints the formatted startup message.
func printStartupMessage(apiEndpoints []string, err error) {
if err != nil {
logStartupMessage(color.RedBold("Server startup failed with '%v'", err))
logStartupMessage(color.RedBold("Not all features may be available on this server"))
logStartupMessage(color.RedBold("Please use 'mc admin' commands to further investigate this issue"))
if globalConsoleSys != nil {
globalConsoleSys.Send(fmt.Sprintf("Server startup failed with '%v', some features may be missing", err), string(logger.All))
}
}
strippedAPIEndpoints := stripStandardPorts(apiEndpoints, globalMinioHost)
@@ -76,7 +76,7 @@ func printStartupMessage(apiEndpoints []string, err error) {
if globalMinioConsolePortAuto && globalBrowserEnabled {
msg := fmt.Sprintf("\nWARNING: Console endpoint is listening on a dynamic port (%s), please use --console-address \":PORT\" to choose a static port.",
globalMinioConsolePort)
logStartupMessage(color.RedBold(msg))
logger.Info(color.RedBold(msg))
}
}
@@ -131,29 +131,29 @@ func printServerCommonMsg(apiEndpoints []string) {
apiEndpointStr := strings.Join(apiEndpoints, " ")
// Colorize the message and print.
logStartupMessage(color.Blue("API: ") + color.Bold(fmt.Sprintf("%s ", apiEndpointStr)))
if color.IsTerminal() && !globalCLIContext.Anonymous {
logStartupMessage(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logStartupMessage(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
logger.Info(color.Blue("API: ") + color.Bold(fmt.Sprintf("%s ", apiEndpointStr)))
if color.IsTerminal() && (!globalCLIContext.Anonymous && !globalCLIContext.JSON) {
logger.Info(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logger.Info(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
if region != "" {
logStartupMessage(color.Blue("Region: ") + color.Bold(fmt.Sprintf(getFormatStr(len(region), 2), region)))
logger.Info(color.Blue("Region: ") + color.Bold(fmt.Sprintf(getFormatStr(len(region), 2), region)))
}
}
printEventNotifiers()
if globalBrowserEnabled {
consoleEndpointStr := strings.Join(stripStandardPorts(getConsoleEndpoints(), globalMinioConsoleHost), " ")
logStartupMessage(color.Blue("\nConsole: ") + color.Bold(fmt.Sprintf("%s ", consoleEndpointStr)))
if color.IsTerminal() && !globalCLIContext.Anonymous {
logStartupMessage(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logStartupMessage(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
logger.Info(color.Blue("\nConsole: ") + color.Bold(fmt.Sprintf("%s ", consoleEndpointStr)))
if color.IsTerminal() && (!globalCLIContext.Anonymous && !globalCLIContext.JSON) {
logger.Info(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
logger.Info(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
}
}
}
// Prints startup message for Object API acces, prints link to our SDK documentation.
func printObjectAPIMsg() {
logStartupMessage(color.Blue("\nDocumentation: ") + "https://docs.min.io")
logger.Info(color.Blue("\nDocumentation: ") + "https://docs.min.io")
}
// Prints bucket notification configurations.
@@ -172,7 +172,7 @@ func printEventNotifiers() {
arnMsg += color.Bold(fmt.Sprintf("%s ", arn))
}
logStartupMessage(arnMsg)
logger.Info(arnMsg)
}
// Prints startup message for command line access. Prints link to our documentation
@@ -185,15 +185,15 @@ func printCLIAccessMsg(endPoint string, alias string) {
// Configure 'mc', following block prints platform specific information for minio client.
if color.IsTerminal() && !globalCLIContext.Anonymous {
logStartupMessage(color.Blue("\nCommand-line: ") + mcQuickStartGuide)
logger.Info(color.Blue("\nCommand-line: ") + mcQuickStartGuide)
if runtime.GOOS == globalWindowsOSName {
mcMessage := fmt.Sprintf("$ mc.exe alias set %s %s %s %s", alias,
endPoint, cred.AccessKey, cred.SecretKey)
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
logger.Info(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
} else {
mcMessage := fmt.Sprintf("$ mc alias set %s %s %s %s", alias,
endPoint, cred.AccessKey, cred.SecretKey)
logStartupMessage(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
logger.Info(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
}
}
}
@@ -220,10 +220,7 @@ func getStorageInfoMsg(storageInfo StorageInfo) string {
// Prints startup message of storage capacity and erasure information.
func printStorageInfo(storageInfo StorageInfo) {
if msg := getStorageInfoMsg(storageInfo); msg != "" {
if globalCLIContext.Quiet {
logger.Info(msg)
}
logStartupMessage(msg)
logger.Info(msg)
}
}
@@ -231,5 +228,5 @@ func printCacheStorageInfo(storageInfo CacheStorageInfo) {
msg := fmt.Sprintf("%s %s Free, %s Total", color.Blue("Cache Capacity:"),
humanize.IBytes(storageInfo.Free),
humanize.IBytes(storageInfo.Total))
logStartupMessage(msg)
logger.Info(msg)
}
+3 -18
View File
@@ -20,13 +20,11 @@ package cmd
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net/http"
"net/url"
"reflect"
"sort"
@@ -191,7 +189,7 @@ func (c *SiteReplicationSys) Init(ctx context.Context, objAPI ObjectLayer) error
c.RLock()
defer c.RUnlock()
if c.enabled {
logger.Info("Cluster Replication initialized.")
logger.Info("Cluster replication initialized")
}
return err
@@ -2040,26 +2038,13 @@ func (c *SiteReplicationSys) RemoveRemoteTargetsForEndpoint(ctx context.Context,
// Other helpers
// newRemoteClusterHTTPTransport returns a new http configuration
// used while communicating with the remote cluster.
func newRemoteClusterHTTPTransport() *http.Transport {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
RootCAs: globalRootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
},
}
return tr
}
func getAdminClient(endpoint, accessKey, secretKey string) (*madmin.AdminClient, error) {
epURL, _ := url.Parse(endpoint)
client, err := madmin.New(epURL.Host, accessKey, secretKey, epURL.Scheme == "https")
if err != nil {
return nil, err
}
client.SetCustomTransport(newRemoteClusterHTTPTransport())
client.SetCustomTransport(NewRemoteTargetHTTPTransport())
return client, nil
}
@@ -2071,7 +2056,7 @@ func getS3Client(pc madmin.PeerSite) (*minioClient.Client, error) {
return minioClient.New(ep.Host, &minioClient.Options{
Creds: credentials.NewStaticV4(pc.AccessKey, pc.SecretKey, ""),
Secure: ep.Scheme == "https",
Transport: newRemoteClusterHTTPTransport(),
Transport: NewRemoteTargetHTTPTransport(),
})
}
+9 -19
View File
@@ -19,8 +19,6 @@ package cmd
import (
"context"
"os"
"path/filepath"
"sort"
"time"
@@ -28,8 +26,6 @@ import (
"github.com/minio/madmin-go"
)
var dperfUUID = mustGetUUID()
type speedTestOpts struct {
throughputSize int
concurrencyStart int
@@ -171,29 +167,23 @@ func driveSpeedTest(ctx context.Context, opts madmin.DriveSpeedTestOpts) madmin.
BlockSize: opts.BlockSize,
FileSize: opts.FileSize,
}
paths := func() []string {
toRet := []string{}
paths := globalEndpoints.LocalDisksPaths()
for _, p := range paths {
dperfFilename := filepath.Join(p, ".minio.sys", "tmp", dperfUUID)
if _, err := os.Stat(dperfFilename); err == nil {
// file exists, so remove it
os.Remove(dperfFilename)
}
toRet = append(toRet, dperfFilename)
localPaths := globalEndpoints.LocalDisksPaths()
paths := func() (tmpPaths []string) {
for _, lp := range localPaths {
tmpPaths = append(tmpPaths, pathJoin(lp, minioMetaTmpBucket))
}
return toRet
return tmpPaths
}()
perfs, err := perf.Run(ctx, paths...)
return madmin.DriveSpeedTestResult{
Endpoint: globalLocalNodeName,
Version: Version,
DrivePerf: func() []madmin.DrivePerf {
results := []madmin.DrivePerf{}
for _, r := range perfs {
DrivePerf: func() (results []madmin.DrivePerf) {
for idx, r := range perfs {
result := madmin.DrivePerf{
Path: r.Path,
Path: localPaths[idx],
ReadThroughput: r.ReadThroughput,
WriteThroughput: r.WriteThroughput,
Error: func() string {
+29 -7
View File
@@ -29,18 +29,40 @@ type StorageAPI interface {
String() string
// Storage operations.
IsOnline() bool // Returns true if disk is online.
LastConn() time.Time // Returns the last time this disk (re)-connected
// Returns true if disk is online and its valid i.e valid format.json.
// This has nothing to do with if the drive is hung or not responding.
// For that individual storage API calls will fail properly. The purpose
// of this function is to know if the "drive" has "format.json" or not
// if it has a "format.json" then is it correct "format.json" or not.
IsOnline() bool
// Returns the last time this disk (re)-connected
LastConn() time.Time
// Indicates if disk is local or not.
IsLocal() bool
Hostname() string // Returns host name if remote host.
Endpoint() Endpoint // Returns endpoint.
// Returns hostname if disk is remote.
Hostname() string
// Returns the entire endpoint.
Endpoint() Endpoint
// Close the disk, mark it purposefully closed, only implemented for remote disks.
Close() error
GetDiskID() (string, error)
SetDiskID(id string)
Healing() *healingTracker // Returns nil if disk is not healing.
// Returns the unique 'uuid' of this disk.
GetDiskID() (string, error)
// Set a unique 'uuid' for this disk, only used when
// disk is replaced and formatted.
SetDiskID(id string)
// Returns healing information for a newly replaced disk,
// returns 'nil' once healing is complete or if the disk
// has never been replaced.
Healing() *healingTracker
DiskInfo(ctx context.Context) (info DiskInfo, err error)
NSScanner(ctx context.Context, cache dataUsageCache, updates chan<- dataUsageEntry) (dataUsageCache, error)
+14 -4
View File
@@ -52,7 +52,7 @@ var errDiskStale = errors.New("disk stale")
// To abstract a disk over network.
type storageRESTServer struct {
storage *xlStorage
storage *xlStorageDiskIDCheck
}
func (s *storageRESTServer) writeErrorResponse(w http.ResponseWriter, err error) {
@@ -109,7 +109,7 @@ func storageServerRequestValidate(r *http.Request) error {
}
// IsValid - To authenticate and verify the time difference.
func (s *storageRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
func (s *storageRESTServer) IsAuthValid(w http.ResponseWriter, r *http.Request) bool {
if s.storage == nil {
s.writeErrorResponse(w, errDiskNotFound)
return false
@@ -120,6 +120,15 @@ func (s *storageRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool
return false
}
return true
}
// IsValid - To authenticate and check if the disk-id in the request corresponds to the underlying disk.
func (s *storageRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
if !s.IsAuthValid(w, r) {
return false
}
if err := r.ParseForm(); err != nil {
s.writeErrorResponse(w, err)
return false
@@ -155,7 +164,7 @@ func (s *storageRESTServer) HealthHandler(w http.ResponseWriter, r *http.Request
// DiskInfoHandler - returns disk info.
func (s *storageRESTServer) DiskInfoHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
if !s.IsAuthValid(w, r) {
return
}
info, err := s.storage.DiskInfo(r.Context())
@@ -1224,7 +1233,8 @@ func registerStorageRESTHandlers(router *mux.Router, endpointServerPools Endpoin
endpoint := storage.Endpoint()
server := &storageRESTServer{storage: storage}
server := &storageRESTServer{storage: newXLStorageDiskIDCheck(storage)}
server.storage.SetDiskID(storage.diskID)
subrouter := router.PathPrefix(path.Join(storageRESTPrefix, endpoint.Path)).Subrouter()
BIN
View File
Binary file not shown.
+63
View File
@@ -205,6 +205,69 @@ func (api adminAPIHandlers) EditTierHandler(w http.ResponseWriter, r *http.Reque
writeSuccessNoContent(w)
}
func (api adminAPIHandlers) RemoveTierHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "RemoveTier")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
objAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetTierAction)
if objAPI == nil || globalNotificationSys == nil || globalTierConfigMgr == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
vars := mux.Vars(r)
tier := vars["tier"]
if err := globalTierConfigMgr.Reload(ctx, objAPI); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err := globalTierConfigMgr.Remove(ctx, tier); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
if err := globalTierConfigMgr.Save(ctx, objAPI); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
globalNotificationSys.LoadTransitionTierConfig(ctx)
writeSuccessNoContent(w)
}
func (api adminAPIHandlers) VerifyTierHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "VerifyTier")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
if !globalIsErasure {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
return
}
objAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListTierAction)
if objAPI == nil || globalNotificationSys == nil || globalTierConfigMgr == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
return
}
vars := mux.Vars(r)
tier := vars["tier"]
if err := globalTierConfigMgr.Verify(ctx, tier); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
writeSuccessNoContent(w)
}
func (api adminAPIHandlers) TierStatsHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "TierStats")
+33
View File
@@ -40,6 +40,7 @@ var (
errTierInsufficientCreds = errors.New("insufficient tier credentials supplied")
errTierBackendInUse = errors.New("remote tier backend already in use")
errTierTypeUnsupported = errors.New("unsupported tier type")
errTierBackendNotEmpty = errors.New("remote tier not empty")
)
const (
@@ -116,8 +117,40 @@ func (config *TierConfigMgr) Add(ctx context.Context, tier madmin.TierConfig) er
return nil
}
// Remove removes tier if it is empty.
func (config *TierConfigMgr) Remove(ctx context.Context, tier string) error {
d, err := config.getDriver(tier)
if err != nil {
return err
}
if inuse, err := d.InUse(ctx); err != nil {
return err
} else if inuse {
return errTierBackendNotEmpty
} else {
config.Lock()
delete(config.Tiers, tier)
delete(config.drivercache, tier)
config.Unlock()
}
return nil
}
// Verify verifies if tier's config is valid by performing all supported
// operations on the corresponding warmbackend.
func (config *TierConfigMgr) Verify(ctx context.Context, tier string) error {
d, err := config.getDriver(tier)
if err != nil {
return err
}
return checkWarmBackend(ctx, d)
}
// Empty returns if tier targets are empty
func (config *TierConfigMgr) Empty() bool {
if config == nil {
return true
}
return len(config.ListTiers()) == 0
}
+4
View File
@@ -41,6 +41,10 @@ func prepareUpdateMessage(downloadURL string, older time.Duration) string {
t := time.Time{}
newerThan := humanize.RelTime(t, t.Add(older), "ago", "")
if globalCLIContext.JSON {
return fmt.Sprintf("You are running an older version of MinIO released %s, update: %s", newerThan, downloadURL)
}
// Return the nicely colored and formatted update message.
return colorizeUpdateMessage(downloadURL, newerThan)
}
+1
View File
@@ -633,6 +633,7 @@ func NewGatewayHTTPTransportWithClientCerts(clientCert, clientKey string) *http.
err.Error()))
}
if c != nil {
c.UpdateReloadDuration(10 * time.Second)
c.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate
}
+11 -3
View File
@@ -21,8 +21,10 @@ import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/minio/madmin-go"
@@ -31,6 +33,12 @@ import (
"github.com/minio/minio-go/v7/pkg/credentials"
)
// getRemoteTierTargetInstanceTransport contains a singleton roundtripper.
var (
getRemoteTierTargetInstanceTransport http.RoundTripper
getRemoteTierTargetInstanceTransportOnce sync.Once
)
type warmBackendS3 struct {
client *minio.Client
core *minio.Core
@@ -109,13 +117,13 @@ func newWarmBackendS3(conf madmin.TierS3) (*warmBackendS3, error) {
} else {
creds = credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, "")
}
getRemoteTargetInstanceTransportOnce.Do(func() {
getRemoteTargetInstanceTransport = newGatewayHTTPTransport(10 * time.Minute)
getRemoteTierTargetInstanceTransportOnce.Do(func() {
getRemoteTierTargetInstanceTransport = newGatewayHTTPTransport(10 * time.Minute)
})
opts := &minio.Options{
Creds: creds,
Secure: u.Scheme == "https",
Transport: getRemoteTargetInstanceTransport,
Transport: getRemoteTierTargetInstanceTransport,
}
client, err := minio.New(u.Host, opts)
if err != nil {
+420 -174
View File
@@ -19,13 +19,19 @@ package cmd
import (
"context"
"errors"
"fmt"
"io"
"math/rand"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/minio/madmin-go"
"github.com/minio/minio/internal/logger"
)
//go:generate stringer -type=storageMetric -trimprefix=storageMetric $GOFILE
@@ -65,14 +71,12 @@ const (
// Detects change in underlying disk.
type xlStorageDiskIDCheck struct {
// fields position optimized for memory please
// do not re-order them, if you add new fields
// please use `fieldalignment ./...` to check
// if your changes are not causing any problems.
storage StorageAPI
// apiCalls should be placed first so alignment is guaranteed for atomic operations.
apiCalls [storageMetricLast]uint64
apiLatencies [storageMetricLast]*lockedLastMinuteLatency
diskID string
apiCalls [storageMetricLast]uint64
storage *xlStorage
health *diskHealthTracker
}
func (p *xlStorageDiskIDCheck) getMetrics() DiskMetrics {
@@ -109,6 +113,7 @@ func (e *lockedLastMinuteLatency) value() uint64 {
func newXLStorageDiskIDCheck(storage *xlStorage) *xlStorageDiskIDCheck {
xl := xlStorageDiskIDCheck{
storage: storage,
health: newDiskHealthTracker(),
}
for i := range xl.apiLatencies[:] {
xl.apiLatencies[i] = &lockedLastMinuteLatency{}
@@ -215,25 +220,31 @@ func (p *xlStorageDiskIDCheck) DiskInfo(ctx context.Context) (info DiskInfo, err
return info, errDiskNotFound
}
}
if p.health.isFaulty() {
// if disk is already faulty return faulty for 'mc admin info' output and prometheus alerts.
return info, errFaultyDisk
}
return info, nil
}
func (p *xlStorageDiskIDCheck) MakeVolBulk(ctx context.Context, volumes ...string) (err error) {
defer p.updateStorageMetrics(storageMetricMakeVolBulk, volumes...)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricMakeVolBulk, volumes...)
if err != nil {
return err
}
defer done(&err)
return p.storage.MakeVolBulk(ctx, volumes...)
}
func (p *xlStorageDiskIDCheck) MakeVol(ctx context.Context, volume string) (err error) {
defer p.updateStorageMetrics(storageMetricMakeVol, volume)()
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricMakeVol, volume)
if err != nil {
return err
}
defer done(&err)
if contextCanceled(ctx) {
return ctx.Err()
}
@@ -244,167 +255,122 @@ func (p *xlStorageDiskIDCheck) MakeVol(ctx context.Context, volume string) (err
return p.storage.MakeVol(ctx, volume)
}
func (p *xlStorageDiskIDCheck) ListVols(ctx context.Context) ([]VolInfo, error) {
defer p.updateStorageMetrics(storageMetricListVols, "/")()
if contextCanceled(ctx) {
return nil, ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) ListVols(ctx context.Context) (vi []VolInfo, err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricListVols, "/")
if err != nil {
return nil, err
}
defer done(&err)
return p.storage.ListVols(ctx)
}
func (p *xlStorageDiskIDCheck) StatVol(ctx context.Context, volume string) (vol VolInfo, err error) {
defer p.updateStorageMetrics(storageMetricStatVol, volume)()
if contextCanceled(ctx) {
return VolInfo{}, ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricStatVol, volume)
if err != nil {
return vol, err
}
defer done(&err)
return p.storage.StatVol(ctx, volume)
}
func (p *xlStorageDiskIDCheck) DeleteVol(ctx context.Context, volume string, forceDelete bool) (err error) {
defer p.updateStorageMetrics(storageMetricDeleteVol, volume)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricDeleteVol, volume)
if err != nil {
return err
}
defer done(&err)
return p.storage.DeleteVol(ctx, volume, forceDelete)
}
func (p *xlStorageDiskIDCheck) ListDir(ctx context.Context, volume, dirPath string, count int) ([]string, error) {
defer p.updateStorageMetrics(storageMetricListDir, volume, dirPath)()
if contextCanceled(ctx) {
return nil, ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) ListDir(ctx context.Context, volume, dirPath string, count int) (s []string, err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricListDir, volume, dirPath)
if err != nil {
return nil, err
}
defer done(&err)
return p.storage.ListDir(ctx, volume, dirPath, count)
}
func (p *xlStorageDiskIDCheck) ReadFile(ctx context.Context, volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
defer p.updateStorageMetrics(storageMetricReadFile, volume, path)()
if contextCanceled(ctx) {
return 0, ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricReadFile, volume, path)
if err != nil {
return 0, err
}
defer done(&err)
return p.storage.ReadFile(ctx, volume, path, offset, buf, verifier)
}
func (p *xlStorageDiskIDCheck) AppendFile(ctx context.Context, volume string, path string, buf []byte) (err error) {
defer p.updateStorageMetrics(storageMetricAppendFile, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricAppendFile, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.AppendFile(ctx, volume, path, buf)
}
func (p *xlStorageDiskIDCheck) CreateFile(ctx context.Context, volume, path string, size int64, reader io.Reader) error {
defer p.updateStorageMetrics(storageMetricCreateFile, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) CreateFile(ctx context.Context, volume, path string, size int64, reader io.Reader) (err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricCreateFile, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.CreateFile(ctx, volume, path, size, reader)
}
func (p *xlStorageDiskIDCheck) ReadFileStream(ctx context.Context, volume, path string, offset, length int64) (io.ReadCloser, error) {
defer p.updateStorageMetrics(storageMetricReadFileStream, volume, path)()
if contextCanceled(ctx) {
return nil, ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricReadFileStream, volume, path)
if err != nil {
return nil, err
}
defer done(&err)
return p.storage.ReadFileStream(ctx, volume, path, offset, length)
}
func (p *xlStorageDiskIDCheck) RenameFile(ctx context.Context, srcVolume, srcPath, dstVolume, dstPath string) error {
defer p.updateStorageMetrics(storageMetricRenameFile, srcVolume, srcPath, dstVolume, dstPath)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) RenameFile(ctx context.Context, srcVolume, srcPath, dstVolume, dstPath string) (err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricRenameFile, srcVolume, srcPath, dstVolume, dstPath)
if err != nil {
return err
}
defer done(&err)
return p.storage.RenameFile(ctx, srcVolume, srcPath, dstVolume, dstPath)
}
func (p *xlStorageDiskIDCheck) RenameData(ctx context.Context, srcVolume, srcPath string, fi FileInfo, dstVolume, dstPath string) error {
defer p.updateStorageMetrics(storageMetricRenameData, srcPath, fi.DataDir, dstVolume, dstPath)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) RenameData(ctx context.Context, srcVolume, srcPath string, fi FileInfo, dstVolume, dstPath string) (err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricRenameData, srcPath, fi.DataDir, dstVolume, dstPath)
if err != nil {
return err
}
defer done(&err)
return p.storage.RenameData(ctx, srcVolume, srcPath, fi, dstVolume, dstPath)
}
func (p *xlStorageDiskIDCheck) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (err error) {
defer p.updateStorageMetrics(storageMetricCheckParts, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricCheckParts, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.CheckParts(ctx, volume, path, fi)
}
func (p *xlStorageDiskIDCheck) Delete(ctx context.Context, volume string, path string, recursive bool) (err error) {
defer p.updateStorageMetrics(storageMetricDelete, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricDelete, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.Delete(ctx, volume, path, recursive)
}
@@ -417,136 +383,102 @@ func (p *xlStorageDiskIDCheck) DeleteVersions(ctx context.Context, volume string
if len(versions) > 0 {
path = versions[0].Name
}
defer p.updateStorageMetrics(storageMetricDeleteVersions, volume, path)()
errs = make([]error, len(versions))
if contextCanceled(ctx) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricDeleteVersions, volume, path)
if err != nil {
for i := range errs {
errs[i] = ctx.Err()
}
return errs
}
if err := p.checkDiskStale(); err != nil {
for i := range errs {
errs[i] = err
defer done(&err)
errs = p.storage.DeleteVersions(ctx, volume, versions)
for i := range errs {
if errs[i] != nil {
err = errs[i]
break
}
return errs
}
return p.storage.DeleteVersions(ctx, volume, versions)
return errs
}
func (p *xlStorageDiskIDCheck) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) error {
defer p.updateStorageMetrics(storageMetricVerifyFile, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err := p.checkDiskStale(); err != nil {
func (p *xlStorageDiskIDCheck) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (err error) {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricVerifyFile, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.VerifyFile(ctx, volume, path, fi)
}
func (p *xlStorageDiskIDCheck) WriteAll(ctx context.Context, volume string, path string, b []byte) (err error) {
defer p.updateStorageMetrics(storageMetricWriteAll, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricWriteAll, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.WriteAll(ctx, volume, path, b)
}
func (p *xlStorageDiskIDCheck) DeleteVersion(ctx context.Context, volume, path string, fi FileInfo, forceDelMarker bool) (err error) {
defer p.updateStorageMetrics(storageMetricDeleteVersion, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricDeleteVersion, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.DeleteVersion(ctx, volume, path, fi, forceDelMarker)
}
func (p *xlStorageDiskIDCheck) UpdateMetadata(ctx context.Context, volume, path string, fi FileInfo) (err error) {
defer p.updateStorageMetrics(storageMetricUpdateMetadata, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricUpdateMetadata, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.UpdateMetadata(ctx, volume, path, fi)
}
func (p *xlStorageDiskIDCheck) WriteMetadata(ctx context.Context, volume, path string, fi FileInfo) (err error) {
defer p.updateStorageMetrics(storageMetricWriteMetadata, volume, path)()
if contextCanceled(ctx) {
return ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricWriteMetadata, volume, path)
if err != nil {
return err
}
defer done(&err)
return p.storage.WriteMetadata(ctx, volume, path, fi)
}
func (p *xlStorageDiskIDCheck) ReadVersion(ctx context.Context, volume, path, versionID string, readData bool) (fi FileInfo, err error) {
defer p.updateStorageMetrics(storageMetricReadVersion, volume, path)()
if contextCanceled(ctx) {
return fi, ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricReadVersion, volume, path)
if err != nil {
return fi, err
}
defer done(&err)
return p.storage.ReadVersion(ctx, volume, path, versionID, readData)
}
func (p *xlStorageDiskIDCheck) ReadAll(ctx context.Context, volume string, path string) (buf []byte, err error) {
defer p.updateStorageMetrics(storageMetricReadAll, volume, path)()
if contextCanceled(ctx) {
return nil, ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricReadAll, volume, path)
if err != nil {
return nil, err
}
defer done(&err)
return p.storage.ReadAll(ctx, volume, path)
}
func (p *xlStorageDiskIDCheck) StatInfoFile(ctx context.Context, volume, path string, glob bool) (stat []StatInfo, err error) {
defer p.updateStorageMetrics(storageMetricStatInfoFile, volume, path)()
if contextCanceled(ctx) {
return nil, ctx.Err()
}
if err = p.checkDiskStale(); err != nil {
ctx, done, err := p.TrackDiskHealth(ctx, storageMetricStatInfoFile, volume, path)
if err != nil {
return nil, err
}
defer done(&err)
return p.storage.StatInfoFile(ctx, volume, path, glob)
}
@@ -565,10 +497,10 @@ func storageTrace(s storageMetric, startTime time.Time, duration time.Duration,
}
// Update storage metrics
func (p *xlStorageDiskIDCheck) updateStorageMetrics(s storageMetric, paths ...string) func() {
func (p *xlStorageDiskIDCheck) updateStorageMetrics(s storageMetric, paths ...string) func(err *error) {
startTime := time.Now()
trace := globalTrace.NumSubscribers() > 0
return func() {
return func(err *error) {
duration := time.Since(startTime)
atomic.AddUint64(&p.apiCalls[s], 1)
@@ -579,3 +511,317 @@ func (p *xlStorageDiskIDCheck) updateStorageMetrics(s storageMetric, paths ...st
}
}
}
const (
diskHealthOK = iota
diskHealthFaulty
)
// diskMaxConcurrent is the maximum number of running concurrent operations
// for local and (incoming) remote disk ops respectively.
var diskMaxConcurrent = 512
func init() {
if s, ok := os.LookupEnv("_MINIO_DISK_MAX_CONCURRENT"); ok && s != "" {
var err error
diskMaxConcurrent, err = strconv.Atoi(s)
if err != nil {
logger.Fatal(err, "invalid _MINIO_DISK_MAX_CONCURRENT value")
}
}
}
type diskHealthTracker struct {
// atomic time of last success
lastSuccess int64
// atomic time of last time a token was grabbed.
lastStarted int64
// Atomic status of disk.
status int32
// Atomic number of requests blocking for a token.
blocked int32
// Concurrency tokens.
tokens chan struct{}
}
// newDiskHealthTracker creates a new disk health tracker.
func newDiskHealthTracker() *diskHealthTracker {
d := diskHealthTracker{
lastSuccess: time.Now().UnixNano(),
lastStarted: time.Now().UnixNano(),
status: diskHealthOK,
tokens: make(chan struct{}, diskMaxConcurrent),
}
for i := 0; i < diskMaxConcurrent; i++ {
d.tokens <- struct{}{}
}
return &d
}
// logSuccess will update the last successful operation time.
func (d *diskHealthTracker) logSuccess() {
atomic.StoreInt64(&d.lastSuccess, time.Now().UnixNano())
}
func (d *diskHealthTracker) isFaulty() bool {
return atomic.LoadInt32(&d.status) == diskHealthFaulty
}
type (
healthDiskCtxKey struct{}
healthDiskCtxValue struct {
lastSuccess *int64
}
)
// logSuccess will update the last successful operation time.
func (h *healthDiskCtxValue) logSuccess() {
atomic.StoreInt64(h.lastSuccess, time.Now().UnixNano())
}
// noopDoneFunc is a no-op done func.
// Can be reused.
var noopDoneFunc = func(_ *error) {}
// TrackDiskHealth for this request.
// When a non-nil error is returned 'done' MUST be called
// with the status of the response, if it corresponds to disk health.
// If the pointer sent to done is non-nil AND the error
// is either nil or io.EOF the disk is considered good.
// So if unsure if the disk status is ok, return nil as a parameter to done.
// Shadowing will work as long as return error is named: https://go.dev/play/p/sauq86SsTN2
func (p *xlStorageDiskIDCheck) TrackDiskHealth(ctx context.Context, s storageMetric, paths ...string) (c context.Context, done func(*error), err error) {
done = noopDoneFunc
if contextCanceled(ctx) {
return ctx, done, ctx.Err()
}
// Return early if disk is faulty already.
if atomic.LoadInt32(&p.health.status) == diskHealthFaulty {
return ctx, done, errFaultyDisk
}
// Verify if the disk is not stale
// - missing format.json (unformatted drive)
// - format.json is valid but invalid 'uuid'
if err = p.checkDiskStale(); err != nil {
return ctx, done, err
}
// Disallow recursive tracking to avoid deadlocks.
if ctx.Value(healthDiskCtxKey{}) != nil {
done = p.updateStorageMetrics(s, paths...)
return ctx, done, nil
}
select {
case <-ctx.Done():
return ctx, done, ctx.Err()
case <-p.health.tokens:
// Fast path, got token.
default:
// We ran out of tokens, check health before blocking.
err = p.waitForToken(ctx)
if err != nil {
return ctx, done, err
}
}
// We only progress here if we got a token.
atomic.StoreInt64(&p.health.lastStarted, time.Now().UnixNano())
ctx = context.WithValue(ctx, healthDiskCtxKey{}, &healthDiskCtxValue{lastSuccess: &p.health.lastSuccess})
si := p.updateStorageMetrics(s, paths...)
t := time.Now()
var once sync.Once
return ctx, func(errp *error) {
once.Do(func() {
if false {
var ers string
if errp != nil {
err := *errp
ers = fmt.Sprint(err)
}
fmt.Println(time.Now().Format(time.RFC3339), "op", s, "took", time.Since(t), "result:", ers, "disk:", p.storage.String(), "path:", strings.Join(paths, "/"))
}
p.health.tokens <- struct{}{}
if errp != nil {
err := *errp
if err != nil && !errors.Is(err, io.EOF) {
return
}
p.health.logSuccess()
}
si(errp)
})
}, nil
}
// waitForToken will wait for a token, while periodically
// checking the disk status.
// If nil is returned a token was picked up.
func (p *xlStorageDiskIDCheck) waitForToken(ctx context.Context) (err error) {
atomic.AddInt32(&p.health.blocked, 1)
defer func() {
atomic.AddInt32(&p.health.blocked, -1)
}()
// Avoid stampeding herd...
ticker := time.NewTicker(5*time.Second + time.Duration(rand.Int63n(int64(5*time.Second))))
defer ticker.Stop()
for {
err = p.checkHealth(ctx)
if err != nil {
return err
}
select {
case <-ticker.C:
// Ticker expired, check health again.
case <-ctx.Done():
return ctx.Err()
case <-p.health.tokens:
return nil
}
}
}
// checkHealth should only be called when tokens have run out.
// This will check if disk should be taken offline.
func (p *xlStorageDiskIDCheck) checkHealth(ctx context.Context) (err error) {
if atomic.LoadInt32(&p.health.status) == diskHealthFaulty {
return errFaultyDisk
}
// Check if there are tokens.
if len(p.health.tokens) > 0 {
return nil
}
const maxTimeSinceLastSuccess = 30 * time.Second
const minTimeSinceLastOpStarted = 15 * time.Second
// To avoid stampeding herd (100s of simultaneous starting requests)
// there must be a delay between the last started request and now
// for the last lastSuccess to be useful.
t := time.Since(time.Unix(0, atomic.LoadInt64(&p.health.lastStarted)))
if t < minTimeSinceLastOpStarted {
return nil
}
// If also more than 15 seconds since last success, take disk offline.
t = time.Since(time.Unix(0, atomic.LoadInt64(&p.health.lastSuccess)))
if t > maxTimeSinceLastSuccess {
if atomic.CompareAndSwapInt32(&p.health.status, diskHealthOK, diskHealthFaulty) {
logger.LogAlwaysIf(ctx, fmt.Errorf("taking disk %s offline, time since last response %v", p.storage.String(), t.Round(time.Millisecond)))
go p.monitorDiskStatus()
}
return errFaultyDisk
}
return nil
}
// monitorDiskStatus should be called once when a drive has been marked offline.
// Once the disk has been deemed ok, it will return to online status.
func (p *xlStorageDiskIDCheck) monitorDiskStatus() {
t := time.NewTicker(5 * time.Second)
defer t.Stop()
fn := mustGetUUID()
for range t.C {
if len(p.health.tokens) == 0 {
// Queue is still full, no need to check.
continue
}
err := p.storage.WriteAll(context.Background(), minioMetaTmpBucket, fn, []byte{10000: 42})
if err != nil {
continue
}
b, err := p.storage.ReadAll(context.Background(), minioMetaTmpBucket, fn)
if err != nil || len(b) != 10001 {
continue
}
err = p.storage.Delete(context.Background(), minioMetaTmpBucket, fn, false)
if err == nil {
logger.Info("Able to read+write, bringing disk %s online.", p.storage.String())
atomic.StoreInt32(&p.health.status, diskHealthOK)
return
}
}
}
// diskHealthCheckOK will check if the provided error is nil
// and update disk status if good.
// For convenience a bool is returned to indicate any error state
// that is not io.EOF.
func diskHealthCheckOK(ctx context.Context, err error) bool {
// Check if context has a disk health check.
tracker, ok := ctx.Value(healthDiskCtxKey{}).(*healthDiskCtxValue)
if !ok {
// No tracker, return
return err == nil || errors.Is(err, io.EOF)
}
if err == nil || errors.Is(err, io.EOF) {
tracker.logSuccess()
return true
}
return false
}
// diskHealthWrapper provides either a io.Reader or io.Writer
// that updates status of the provided tracker.
// Use through diskHealthReader or diskHealthWriter.
type diskHealthWrapper struct {
tracker *healthDiskCtxValue
r io.Reader
w io.Writer
}
func (d *diskHealthWrapper) Read(p []byte) (int, error) {
if d.r == nil {
return 0, fmt.Errorf("diskHealthWrapper: Read with no reader")
}
n, err := d.r.Read(p)
if err == nil || err == io.EOF && n > 0 {
d.tracker.logSuccess()
}
return n, err
}
func (d *diskHealthWrapper) Write(p []byte) (int, error) {
if d.w == nil {
return 0, fmt.Errorf("diskHealthWrapper: Write with no writer")
}
n, err := d.w.Write(p)
if err == nil && n == len(p) {
d.tracker.logSuccess()
}
return n, err
}
// diskHealthReader provides a wrapper that will update disk health on
// ctx, on every successful read.
// This should only be used directly at the os/syscall level,
// otherwise buffered operations may return false health checks.
func diskHealthReader(ctx context.Context, r io.Reader) io.Reader {
// Check if context has a disk health check.
tracker, ok := ctx.Value(healthDiskCtxKey{}).(*healthDiskCtxValue)
if !ok {
// No need to wrap
return r
}
return &diskHealthWrapper{r: r, tracker: tracker}
}
// diskHealthWriter provides a wrapper that will update disk health on
// ctx, on every successful write.
// This should only be used directly at the os/syscall level,
// otherwise buffered operations may return false health checks.
func diskHealthWriter(ctx context.Context, w io.Writer) io.Writer {
// Check if context has a disk health check.
tracker, ok := ctx.Value(healthDiskCtxKey{}).(*healthDiskCtxValue)
if !ok {
// No need to wrap
return w
}
return &diskHealthWrapper{w: w, tracker: tracker}
}
+37 -1
View File
@@ -18,6 +18,8 @@
package cmd
import (
"errors"
"github.com/zeebo/xxh3"
)
@@ -57,7 +59,19 @@ func getAllFileInfoVersions(xlMetaBuf []byte, volume, path string) (FileInfoVers
}
versions, err = xlMeta.ListVersions(volume, path)
}
if err != nil || len(versions) == 0 {
if err == nil && len(versions) == 0 {
// This special case is needed to handle len(xlMeta.versions) == 0
versions = []FileInfo{
{
Volume: volume,
Name: path,
Deleted: true,
IsLatest: true,
ModTime: timeSentinel1970,
},
}
}
if err != nil {
return FileInfoVersions{}, err
}
@@ -76,11 +90,33 @@ func getFileInfo(xlMetaBuf []byte, volume, path, versionID string, data bool) (F
if buf, data := isIndexedMetaV2(xlMetaBuf); buf != nil {
inData = data
fi, err = buf.ToFileInfo(volume, path, versionID)
if len(buf) != 0 && errors.Is(err, errFileNotFound) {
// This special case is needed to handle len(xlMeta.versions) == 0
return FileInfo{
Volume: volume,
Name: path,
VersionID: versionID,
Deleted: true,
IsLatest: true,
ModTime: timeSentinel1970,
}, nil
}
} else {
var xlMeta xlMetaV2
if err := xlMeta.LoadOrConvert(xlMetaBuf); err != nil {
return FileInfo{}, err
}
if len(xlMeta.versions) == 0 {
// This special case is needed to handle len(xlMeta.versions) == 0
return FileInfo{
Volume: volume,
Name: path,
VersionID: versionID,
Deleted: true,
IsLatest: true,
ModTime: timeSentinel1970,
}, nil
}
inData = xlMeta.data
fi, err = xlMeta.ToFileInfo(volume, path, versionID)
}
+47 -23
View File
@@ -595,6 +595,7 @@ func (j xlMetaV2Object) ToFileInfo(volume, path string) (FileInfo, error) {
}
}
fi.ReplicationState = getInternalReplicationState(fi.Metadata)
fi.Deleted = !fi.VersionPurgeStatus().Empty()
replStatus := fi.ReplicationState.CompositeReplicationStatus()
if replStatus != "" {
fi.Metadata[xhttp.AmzBucketReplicationStatus] = string(replStatus)
@@ -658,7 +659,7 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
buf := metaDataPoolGet()[:initial]
_, err := io.ReadFull(r, buf)
if err != nil {
return nil, fmt.Errorf("readXLMetaNoData.ReadFull: %w", err)
return nil, fmt.Errorf("readXLMetaNoData(io.ReadFull): %w", err)
}
readMore := func(n int64) error {
has := int64(len(buf))
@@ -679,9 +680,9 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
if err != nil {
if errors.Is(err, io.EOF) {
// Returned if we read nothing.
return fmt.Errorf("readXLMetaNoData.readMore: %w", io.ErrUnexpectedEOF)
err = io.ErrUnexpectedEOF
}
return fmt.Errorf("readXLMetaNoData.readMore: %w", err)
return fmt.Errorf("readXLMetaNoData(readMore): %w", err)
}
return nil
}
@@ -699,7 +700,7 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
case 1, 2, 3:
sz, tmp, err := msgp.ReadBytesHeader(tmp)
if err != nil {
return nil, err
return nil, fmt.Errorf("readXLMetaNoData(read_meta): uknown metadata version %w", err)
}
want := int64(sz) + int64(len(buf)-len(tmp))
@@ -720,10 +721,14 @@ func readXLMetaNoData(r io.Reader, size int64) ([]byte, error) {
return nil, err
}
if int64(len(buf)) < want {
return nil, fmt.Errorf("buffer shorter than expected (buflen: %d, want: %d): %w", len(buf), want, errFileCorrupt)
}
tmp = buf[want:]
_, after, err := msgp.ReadUint32Bytes(tmp)
if err != nil {
return nil, err
return nil, fmt.Errorf("readXLMetaNoData(read_meta): unknown metadata version %w", err)
}
want += int64(len(tmp) - len(after))
@@ -880,7 +885,7 @@ func (x *xlMetaV2) loadIndexed(buf xlMetaBuf, data xlMetaInlineData) error {
x.metaV = metaV
if err = x.data.validate(); err != nil {
x.data.repair()
logger.Info("xlMetaV2.loadIndexed: data validation failed: %v. %d entries after repair", err, x.data.entries())
logger.LogIf(GlobalContext, fmt.Errorf("xlMetaV2.loadIndexed: data validation failed: %v. %d entries after repair", err, x.data.entries()))
}
return decodeVersions(buf, versions, func(i int, hdr, meta []byte) error {
@@ -929,7 +934,7 @@ func (x *xlMetaV2) loadLegacy(buf []byte) error {
x.data = buf
if err = x.data.validate(); err != nil {
x.data.repair()
logger.Info("xlMetaV2.Load: data validation failed: %v. %d entries after repair", err, x.data.entries())
logger.LogIf(GlobalContext, fmt.Errorf("xlMetaV2.Load: data validation failed: %v. %d entries after repair", err, x.data.entries()))
}
default:
return errors.New("unknown minor metadata version")
@@ -1150,7 +1155,7 @@ func (x *xlMetaV2) sortByModTime() {
// DeleteVersion deletes the version specified by version id.
// returns to the caller which dataDir to delete, also
// indicates if this is the last version.
func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, error) {
// This is a situation where versionId is explicitly
// specified as "null", as we do not save "null"
// string it is considered empty. But empty also
@@ -1164,7 +1169,7 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
if fi.VersionID != "" {
uv, err = uuid.Parse(fi.VersionID)
if err != nil {
return "", false, errFileVersionNotFound
return "", errFileVersionNotFound
}
}
@@ -1179,7 +1184,7 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
},
}
if !ventry.Valid() {
return "", false, errors.New("internal error: invalid version entry generated")
return "", errors.New("internal error: invalid version entry generated")
}
}
updateVersion := false
@@ -1225,18 +1230,18 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
case LegacyType:
ver, err := x.getIdx(i)
if err != nil {
return "", false, err
return "", err
}
x.versions = append(x.versions[:i], x.versions[i+1:]...)
if fi.Deleted {
err = x.addVersion(ventry)
}
return ver.ObjectV1.DataDir, len(x.versions) == 0, err
return ver.ObjectV1.DataDir, err
case DeleteType:
if updateVersion {
ver, err := x.getIdx(i)
if err != nil {
return "", false, err
return "", err
}
if len(ver.DeleteMarker.MetaSys) == 0 {
ver.DeleteMarker.MetaSys = make(map[string][]byte)
@@ -1258,26 +1263,26 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
ver.DeleteMarker.MetaSys[k] = []byte(v)
}
err = x.setIdx(i, *ver)
return "", len(x.versions) == 0, err
return "", err
}
var err error
x.versions = append(x.versions[:i], x.versions[i+1:]...)
if fi.MarkDeleted && (fi.VersionPurgeStatus().Empty() || (fi.VersionPurgeStatus() != Complete)) {
err = x.addVersion(ventry)
}
return "", len(x.versions) == 0, err
return "", err
case ObjectType:
if updateVersion && !fi.Deleted {
ver, err := x.getIdx(i)
if err != nil {
return "", false, err
return "", err
}
ver.ObjectV2.MetaSys[VersionPurgeStatusKey] = []byte(fi.ReplicationState.VersionPurgeStatusInternal)
for k, v := range fi.ReplicationState.ResetStatusesMap {
ver.ObjectV2.MetaSys[k] = []byte(v)
}
err = x.setIdx(i, *ver)
return "", len(x.versions) == 0, err
return uuid.UUID(ver.ObjectV2.DataDir).String(), err
}
}
}
@@ -1288,7 +1293,7 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
}
ver, err := x.getIdx(i)
if err != nil {
return "", false, err
return "", err
}
switch {
case fi.ExpireRestored:
@@ -1314,16 +1319,16 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
if x.SharedDataDirCount(ver.ObjectV2.VersionID, ver.ObjectV2.DataDir) > 0 {
// Found that another version references the same dataDir
// we shouldn't remove it, and only remove the version instead
return "", len(x.versions) == 0, nil
return "", nil
}
return uuid.UUID(ver.ObjectV2.DataDir).String(), len(x.versions) == 0, err
return uuid.UUID(ver.ObjectV2.DataDir).String(), err
}
if fi.Deleted {
err = x.addVersion(ventry)
return "", false, err
return "", err
}
return "", false, errFileVersionNotFound
return "", errFileVersionNotFound
}
// xlMetaDataDirDecoder is a shallow decoder for decoding object datadir only.
@@ -1686,7 +1691,7 @@ func (x xlMetaV2) ListVersions(volume, path string) ([]FileInfo, error) {
// Quorum must be the minimum number of matching metadata files.
// Quorum should be > 1 and <= len(versions).
// If strict is set to false, entries that match type
func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVersion) (merged []xlMetaV2ShallowVersion) {
func mergeXLV2Versions(quorum int, strict bool, requestedVersions int, versions ...[]xlMetaV2ShallowVersion) (merged []xlMetaV2ShallowVersion) {
if quorum <= 0 {
quorum = 1
}
@@ -1703,6 +1708,8 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
// Shallow copy input
versions = append(make([][]xlMetaV2ShallowVersion, 0, len(versions)), versions...)
var nVersions int // captures all non-free versions
// Our result
merged = make([]xlMetaV2ShallowVersion, 0, len(versions[0]))
tops := make([]xlMetaV2ShallowVersion, len(versions))
@@ -1739,6 +1746,12 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
latest = tops[0]
latestCount = len(tops)
merged = append(merged, latest)
// Calculate latest 'n' non-free versions.
if !latest.header.FreeVersion() {
nVersions++
}
} else {
// Find latest.
for i, ver := range tops {
@@ -1802,6 +1815,11 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
}
if latestCount >= quorum {
merged = append(merged, latest)
// Calculate latest 'n' non-free versions.
if !latest.header.FreeVersion() {
nVersions++
}
}
}
@@ -1835,7 +1853,13 @@ func mergeXLV2Versions(quorum int, strict bool, versions ...[]xlMetaV2ShallowVer
break
}
}
if requestedVersions > 0 && requestedVersions == nVersions {
merged = append(merged, versions[0]...)
break
}
}
// Sanity check. Enable if duplicates show up.
if false {
found := make(map[[16]byte]struct{})
+87 -11
View File
@@ -18,10 +18,13 @@
package cmd
import (
"bufio"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"testing"
"time"
@@ -34,6 +37,29 @@ import (
"github.com/minio/minio/internal/ioutil"
)
func TestReadXLMetaNoData(t *testing.T) {
f, err := os.Open("testdata/xl.meta-corrupt.gz")
if err != nil {
t.Fatal(err)
}
defer f.Close()
gz, err := gzip.NewReader(bufio.NewReader(f))
if err != nil {
t.Fatal(err)
}
buf, err := io.ReadAll(gz)
if err != nil {
t.Fatal(err)
}
_, err = readXLMetaNoData(bytes.NewReader(buf), int64(len(buf)))
if err == nil {
t.Fatal("expected error but returned success")
}
}
func TestXLV2FormatData(t *testing.T) {
failOnErr := func(err error) {
t.Helper()
@@ -366,7 +392,7 @@ func TestDeleteVersionWithSharedDataDir(t *testing.T) {
count := len(testCases)
for i := 4; i < len(testCases); i++ {
tc := testCases[i]
dataDir, _, err := xl.DeleteVersion(fileInfos[i])
dataDir, err := xl.DeleteVersion(fileInfos[i])
failOnErr(count+1, err)
if dataDir != tc.expectedDataDir {
t.Fatalf("Expected %s but got %s", tc.expectedDataDir, dataDir)
@@ -375,6 +401,55 @@ func TestDeleteVersionWithSharedDataDir(t *testing.T) {
}
}
func Benchmark_mergeXLV2Versions(b *testing.B) {
data, err := ioutil.ReadFile("testdata/xl.meta-v1.2.zst")
if err != nil {
b.Fatal(err)
}
dec, _ := zstd.NewReader(nil)
data, err = dec.DecodeAll(data, nil)
if err != nil {
b.Fatal(err)
}
var xl xlMetaV2
if err = xl.LoadOrConvert(data); err != nil {
b.Fatal(err)
}
vers := make([][]xlMetaV2ShallowVersion, 16)
for i := range vers {
vers[i] = xl.versions
}
b.Run("requested-none", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
b.SetBytes(855) // number of versions...
for i := 0; i < b.N; i++ {
mergeXLV2Versions(8, false, 0, vers...)
}
})
b.Run("requested-v1", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
b.SetBytes(855) // number of versions...
for i := 0; i < b.N; i++ {
mergeXLV2Versions(8, false, 1, vers...)
}
})
b.Run("requested-v2", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
b.SetBytes(855) // number of versions...
for i := 0; i < b.N; i++ {
mergeXLV2Versions(8, false, 1, vers...)
}
})
}
func Benchmark_xlMetaV2Shallow_Load(b *testing.B) {
data, err := ioutil.ReadFile("testdata/xl.meta-v1.2.zst")
if err != nil {
@@ -398,6 +473,7 @@ func Benchmark_xlMetaV2Shallow_Load(b *testing.B) {
}
}
})
b.Run("indexed", func(b *testing.B) {
var xl xlMetaV2
err = xl.Load(data)
@@ -524,7 +600,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
for i := range vers {
t.Run(fmt.Sprintf("non-strict-q%d", i), func(t *testing.T) {
merged := mergeXLV2Versions(i, false, vers...)
merged := mergeXLV2Versions(i, false, 0, vers...)
if len(merged) == 0 {
t.Error("Did not get any results")
return
@@ -536,7 +612,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
})
t.Run(fmt.Sprintf("strict-q%d", i), func(t *testing.T) {
merged := mergeXLV2Versions(i, true, vers...)
merged := mergeXLV2Versions(i, true, 0, vers...)
if len(merged) == 0 {
t.Error("Did not get any results")
return
@@ -558,7 +634,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, false, vMod...)
merged := mergeXLV2Versions(i, false, 0, vMod...)
if len(merged) == 0 {
t.Error("Did not get any results")
return
@@ -580,7 +656,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, false, vMod...)
merged := mergeXLV2Versions(i, false, 0, vMod...)
if len(merged) == 0 && i < 2 {
t.Error("Did not get any results")
return
@@ -606,7 +682,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, false, vMod...)
merged := mergeXLV2Versions(i, false, 0, vMod...)
if len(merged) == 0 {
t.Error("Did not get any results")
return
@@ -628,7 +704,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, false, vMod...)
merged := mergeXLV2Versions(i, false, 0, vMod...)
if len(merged) == 0 && i < 2 {
t.Error("Did not get any results")
return
@@ -654,7 +730,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, true, vMod...)
merged := mergeXLV2Versions(i, true, 0, vMod...)
if len(merged) == 0 && i < 2 {
t.Error("Did not get any results")
return
@@ -680,7 +756,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, true, vMod...)
merged := mergeXLV2Versions(i, true, 0, vMod...)
if len(merged) == 0 && i < 2 {
t.Error("Did not get any results")
return
@@ -706,7 +782,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, true, vMod...)
merged := mergeXLV2Versions(i, true, 0, vMod...)
if len(merged) == 0 && i < 2 {
t.Error("Did not get any results")
return
@@ -732,7 +808,7 @@ func Test_mergeXLV2Versions(t *testing.T) {
}
vMod = append(vMod, newVers)
}
merged := mergeXLV2Versions(i, true, vMod...)
merged := mergeXLV2Versions(i, true, 0, vMod...)
if len(merged) == 0 && i < 2 {
t.Error("Did not get any results")
return
+1 -1
View File
@@ -433,7 +433,7 @@ func BenchmarkXlMetaV2Shallow(b *testing.B) {
// Update a random version.
fi.VersionID = ids[rng.Intn(size)]
// Delete...
_, _, err = xl.DeleteVersion(fi)
_, err = xl.DeleteVersion(fi)
if err != nil {
b.Fatal(err)
}
+3 -3
View File
@@ -102,7 +102,7 @@ func TestFreeVersion(t *testing.T) {
tierfi.TransitionedObjName = mustGetUUID()
tierfi.TransitionTier = "MINIOTIER-1"
var err error
_, _, err = xl.DeleteVersion(tierfi)
_, err = xl.DeleteVersion(tierfi)
fatalErr(err)
report()
@@ -124,7 +124,7 @@ func TestFreeVersion(t *testing.T) {
newtierfi.TransitionStatus = ""
newtierfi.SetTierFreeVersionID(fvIDs[1])
report()
_, _, err = xl.DeleteVersion(newtierfi)
_, err = xl.DeleteVersion(newtierfi)
report()
fatalErr(err)
@@ -141,7 +141,7 @@ func TestFreeVersion(t *testing.T) {
freefi := newtierfi
for _, fvID := range fvIDs {
freefi.VersionID = fvID
_, _, err = xl.DeleteVersion(freefi)
_, err = xl.DeleteVersion(freefi)
fatalErr(err)
}
report()
+44 -48
View File
@@ -49,11 +49,12 @@ import (
const (
nullVersionID = "null"
// Really large streams threshold per shard.
reallyLargeFileThreshold = 64 * humanize.MiByte // Optimized for HDDs
// Largest streams threshold per shard.
largestFileThreshold = 64 * humanize.MiByte // Optimized for HDDs
// Small file threshold below which data accompanies metadata from storage layer.
smallFileThreshold = 128 * humanize.KiByte // Optimized for NVMe/SSDs
// For hardrives it is possible to set this to a lower value to avoid any
// spike in latency. But currently we are simply keeping it optimal for SSDs.
@@ -212,21 +213,23 @@ func newXLStorage(ep Endpoint) (s *xlStorage, err error) {
}
var rootDisk bool
if globalRootDiskThreshold > 0 {
// Use MINIO_ROOTDISK_THRESHOLD_SIZE to figure out if
// this disk is a root disk.
info, err := disk.GetInfo(path)
if err != nil {
return nil, err
}
if !globalIsCICD {
if globalRootDiskThreshold > 0 {
// Use MINIO_ROOTDISK_THRESHOLD_SIZE to figure out if
// this disk is a root disk.
info, err := disk.GetInfo(path)
if err != nil {
return nil, err
}
// treat those disks with size less than or equal to the
// threshold as rootDisks.
rootDisk = info.Total <= globalRootDiskThreshold
} else {
rootDisk, err = disk.IsRootDisk(path, SlashSeparator)
if err != nil {
return nil, err
// treat those disks with size less than or equal to the
// threshold as rootDisks.
rootDisk = info.Total <= globalRootDiskThreshold
} else {
rootDisk, err = disk.IsRootDisk(path, SlashSeparator)
if err != nil {
return nil, err
}
}
}
@@ -395,6 +398,9 @@ func (s *xlStorage) readMetadataWithDMTime(ctx context.Context, itemPath string)
}
}
buf, err := readXLMetaNoData(f, stat.Size())
if err != nil {
return nil, stat.ModTime().UTC(), fmt.Errorf("%w -> %s", err, itemPath)
}
return buf, stat.ModTime().UTC(), err
}
@@ -681,9 +687,11 @@ func (s *xlStorage) SetDiskID(id string) {
func (s *xlStorage) MakeVolBulk(ctx context.Context, volumes ...string) error {
for _, volume := range volumes {
if err := s.MakeVol(ctx, volume); err != nil && !errors.Is(err, errVolumeExists) {
err := s.MakeVol(ctx, volume)
if err != nil && !errors.Is(err, errVolumeExists) {
return err
}
diskHealthCheckOK(ctx, err)
}
return nil
}
@@ -878,22 +886,15 @@ func (s *xlStorage) deleteVersions(ctx context.Context, volume, path string, fis
return err
}
var (
dataDir string
lastVersion bool
updated bool
)
for _, fi := range fis {
dataDir, lastVersion, err = xlMeta.DeleteVersion(fi)
dataDir, err := xlMeta.DeleteVersion(fi)
if err != nil {
if !fi.Deleted && (err == errFileNotFound || err == errFileVersionNotFound) {
// Ignore these since
// Ignore these since they do not exist
continue
}
return err
}
updated = true
if dataDir != "" {
versionID := fi.VersionID
if versionID == "" {
@@ -916,11 +917,8 @@ func (s *xlStorage) deleteVersions(ctx context.Context, volume, path string, fis
}
}
lastVersion := len(xlMeta.versions) == 0
if !lastVersion {
if !updated {
return nil
}
buf, err = xlMeta.AppendTo(metaDataPoolGet())
defer metaDataPoolPut(buf)
if err != nil {
@@ -931,12 +929,7 @@ func (s *xlStorage) deleteVersions(ctx context.Context, volume, path string, fis
}
// Move xl.meta to trash
filePath := pathJoin(volumeDir, path, xlStorageFormatFile)
if err = checkPathLength(filePath); err != nil {
return err
}
err = s.moveToTrash(filePath, false)
err = s.moveToTrash(pathJoin(volumeDir, path, xlStorageFormatFile), false)
if err == nil || err == errFileNotFound {
s.deleteFile(volumeDir, pathJoin(volumeDir, path), false)
}
@@ -956,6 +949,7 @@ func (s *xlStorage) DeleteVersions(ctx context.Context, volume string, versions
if err := s.deleteVersions(ctx, volume, fiv.Name, fiv.Versions...); err != nil {
errs[i] = err
}
diskHealthCheckOK(ctx, errs[i])
}
return errs
@@ -1019,7 +1013,7 @@ func (s *xlStorage) DeleteVersion(ctx context.Context, volume, path string, fi F
return err
}
dataDir, lastVersion, err := xlMeta.DeleteVersion(fi)
dataDir, err := xlMeta.DeleteVersion(fi)
if err != nil {
return err
}
@@ -1044,7 +1038,7 @@ func (s *xlStorage) DeleteVersion(ctx context.Context, volume, path string, fi F
}
}
if !lastVersion {
if len(xlMeta.versions) != 0 {
buf, err = xlMeta.AppendTo(metaDataPoolGet())
defer metaDataPoolPut(buf)
if err != nil {
@@ -1395,7 +1389,7 @@ func (s *xlStorage) readAllData(ctx context.Context, volumeDir string, filePath
buf = make([]byte, sz)
}
// Read file...
_, err = io.ReadFull(r, buf)
_, err = io.ReadFull(diskHealthReader(ctx, r), buf)
return buf, stat.ModTime().UTC(), osErrToFileErr(err)
}
@@ -1706,7 +1700,7 @@ func (s *xlStorage) ReadFileStream(ctx context.Context, volume, path string, off
r := struct {
io.Reader
io.Closer
}{Reader: io.LimitReader(or, length), Closer: closeWrapper(func() error {
}{Reader: io.LimitReader(diskHealthReader(ctx, or), length), Closer: closeWrapper(func() error {
if !alignment || offset+length%xioutil.DirectioAlignSize != 0 {
// invalidate page-cache for unaligned reads.
if !globalAPIConfig.isDisableODirect() {
@@ -1796,8 +1790,8 @@ func (s *xlStorage) CreateFile(ctx context.Context, volume, path string, fileSiz
}()
var bufp *[]byte
if fileSize > 0 && fileSize >= reallyLargeFileThreshold {
// use a larger 4MiB buffer for really large streams.
if fileSize > 0 && fileSize >= largestFileThreshold {
// use a larger 4MiB buffer for a really large streams.
bufp = xioutil.ODirectPoolXLarge.Get().(*[]byte)
defer xioutil.ODirectPoolXLarge.Put(bufp)
} else {
@@ -1805,7 +1799,7 @@ func (s *xlStorage) CreateFile(ctx context.Context, volume, path string, fileSiz
defer xioutil.ODirectPoolLarge.Put(bufp)
}
written, err := xioutil.CopyAligned(w, r, *bufp, fileSize)
written, err := xioutil.CopyAligned(diskHealthWriter(ctx, w), r, *bufp, fileSize, w)
if err != nil {
return err
}
@@ -2037,7 +2031,8 @@ func skipAccessChecks(volume string) (ok bool) {
// RenameData - rename source path to destination path atomically, metadata and data directory.
func (s *xlStorage) RenameData(ctx context.Context, srcVolume, srcPath string, fi FileInfo, dstVolume, dstPath string) (err error) {
defer func() {
if err != nil {
if err != nil && !contextCanceled(ctx) {
// Only log these errors if context is not yet canceled.
logger.LogIf(ctx, fmt.Errorf("srcVolume: %s, srcPath: %s, dstVolume: %s:, dstPath: %s - error %v",
srcVolume, srcPath,
dstVolume, dstPath,
@@ -2279,8 +2274,9 @@ func (s *xlStorage) RenameData(ctx context.Context, srcVolume, srcPath string, f
// Any failed rename calls un-roll previous transaction.
s.deleteFile(dstVolumeDir, legacyDataPath, true)
}
return err
return osErrToFileErr(err)
}
diskHealthCheckOK(ctx, err)
// renameAll only for objects that have xl.meta not saved inline.
if len(fi.Data) == 0 && fi.Size > 0 {
@@ -2418,7 +2414,7 @@ func (s *xlStorage) RenameFile(ctx context.Context, srcVolume, srcPath, dstVolum
return nil
}
func (s *xlStorage) bitrotVerify(partPath string, partSize int64, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
func (s *xlStorage) bitrotVerify(ctx context.Context, partPath string, partSize int64, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
// Open the file for reading.
file, err := Open(partPath)
if err != nil {
@@ -2433,7 +2429,7 @@ func (s *xlStorage) bitrotVerify(partPath string, partSize int64, algo BitrotAlg
// for healing code to fix this file.
return err
}
return bitrotVerify(file, fi.Size(), partSize, algo, sum, shardSize)
return bitrotVerify(diskHealthReader(ctx, file), fi.Size(), partSize, algo, sum, shardSize)
}
func (s *xlStorage) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (err error) {
@@ -2458,7 +2454,7 @@ func (s *xlStorage) VerifyFile(ctx context.Context, volume, path string, fi File
for _, part := range fi.Parts {
checksumInfo := erasure.GetChecksumInfo(part.Number)
partPath := pathJoin(volumeDir, path, fi.DataDir, fmt.Sprintf("part.%d", part.Number))
if err := s.bitrotVerify(partPath,
if err := s.bitrotVerify(ctx, partPath,
erasure.ShardFileSize(part.Size),
checksumInfo.Algorithm,
checksumInfo.Hash, erasure.ShardSize()); err != nil {
+6 -6
View File
@@ -1884,7 +1884,7 @@ func TestXLStorageVerifyFile(t *testing.T) {
if err := storage.WriteAll(context.Background(), volName, fileName, data); err != nil {
t.Fatal(err)
}
if err := storage.storage.(*xlStorage).bitrotVerify(pathJoin(path, volName, fileName), size, algo, hashBytes, 0); err != nil {
if err := storage.storage.bitrotVerify(context.Background(), pathJoin(path, volName, fileName), size, algo, hashBytes, 0); err != nil {
t.Fatal(err)
}
@@ -1894,12 +1894,12 @@ func TestXLStorageVerifyFile(t *testing.T) {
}
// Check if VerifyFile reports the incorrect file length (the correct length is `size+1`)
if err := storage.storage.(*xlStorage).bitrotVerify(pathJoin(path, volName, fileName), size, algo, hashBytes, 0); err == nil {
if err := storage.storage.bitrotVerify(context.Background(), pathJoin(path, volName, fileName), size, algo, hashBytes, 0); err == nil {
t.Fatal("expected to fail bitrot check")
}
// Check if bitrot fails
if err := storage.storage.(*xlStorage).bitrotVerify(pathJoin(path, volName, fileName), size+1, algo, hashBytes, 0); err == nil {
if err := storage.storage.bitrotVerify(context.Background(), pathJoin(path, volName, fileName), size+1, algo, hashBytes, 0); err == nil {
t.Fatal("expected to fail bitrot check")
}
@@ -1928,7 +1928,7 @@ func TestXLStorageVerifyFile(t *testing.T) {
t.Fatal(err)
}
w.(io.Closer).Close()
if err := storage.storage.(*xlStorage).bitrotVerify(pathJoin(path, volName, fileName), size, algo, nil, shardSize); err != nil {
if err := storage.storage.bitrotVerify(context.Background(), pathJoin(path, volName, fileName), size, algo, nil, shardSize); err != nil {
t.Fatal(err)
}
@@ -1943,10 +1943,10 @@ func TestXLStorageVerifyFile(t *testing.T) {
t.Fatal(err)
}
f.Close()
if err := storage.storage.(*xlStorage).bitrotVerify(pathJoin(path, volName, fileName), size, algo, nil, shardSize); err == nil {
if err := storage.storage.bitrotVerify(context.Background(), pathJoin(path, volName, fileName), size, algo, nil, shardSize); err == nil {
t.Fatal("expected to fail bitrot check")
}
if err := storage.storage.(*xlStorage).bitrotVerify(pathJoin(path, volName, fileName), size+1, algo, nil, shardSize); err == nil {
if err := storage.storage.bitrotVerify(context.Background(), pathJoin(path, volName, fileName), size+1, algo, nil, shardSize); err == nil {
t.Fatal("expected to fail bitrot check")
}
}
+7 -7
View File
@@ -30,18 +30,18 @@ mc admin trace --all --verbose myminio
## Subnet Health
Subnet Health diagnostics help ensure that the underlying infrastructure that runs MinIO is configured correctly, and is functioning properly. This test is one-shot long running one, that is recommended to be run as soon as the cluster is first provisioned, and each time a failure scenario is encountered. Note that the test incurs majority of the available resources on the system. Care must be taken when using this to debug failure scenario, so as to prevent larger outages. Health tests can be triggered using `mc admin subnet health` command.
Subnet Health diagnostics help ensure that the underlying infrastructure that runs MinIO is configured correctly, and is functioning properly. This test is one-shot long running one, that is recommended to be run as soon as the cluster is first provisioned, and each time a failure scenario is encountered. Note that the test incurs majority of the available resources on the system. Care must be taken when using this to debug failure scenario, so as to prevent larger outages. Health tests can be triggered using `mc support diagnostics` command.
Example:
```sh
minio server /data
minio server /data{1...4}
```
The command takes no flags
```sh
mc admin subnet health myminio
mc support diagnostics myminio/
```
The output printed will be of the form
@@ -84,14 +84,14 @@ Executing `xl-meta` will look for an `xl.meta` in the current folder and decode
### Remotely Inspecting backend data
`mc admin inspect` allows collecting files based on *path* from all backend drives. Matching files will be collected in a zip file with their respective host+drive+path. A MinIO host from October 2021 or later is required for full functionality. Syntax is `mc admin inspect ALIAS/path/to/files`. This can for example be used to collect `xl.meta` from objects that are misbehaving. To collect `xl.meta` from a specific object, for example placed at `ALIAS/bucket/path/to/file.txt` append `/xl.meta`, for instance `mc admin inspect ALIAS/bucket/path/to/file.txt/xl.meta`. All files can be collected, so this can also be used to retrieve `part.*` files, etc.
`mc support inspect` allows collecting files based on *path* from all backend drives. Matching files will be collected in a zip file with their respective host+drive+path. A MinIO host from October 2021 or later is required for full functionality. Syntax is `mc support inspect ALIAS/path/to/files`. This can for example be used to collect `xl.meta` from objects that are misbehaving. To collect `xl.meta` from a specific object, for example placed at `ALIAS/bucket/path/to/file.txt` append `/xl.meta`, for instance `mc support inspect ALIAS/bucket/path/to/file.txt/xl.meta`. All files can be collected, so this can also be used to retrieve `part.*` files, etc.
Wildcards can be used, for example `mc admin inspect ALIAS/bucket/path/**/xl.meta` will collect all `xl.meta` recursively. `mc admin inspect ALIAS/bucket/path/to/file.txt/*/part.*` will collect parts for all versions for the object located at `bucket/path/to/file.txt`.
Wildcards can be used, for example `mc support inspect ALIAS/bucket/path/**/xl.meta` will collect all `xl.meta` recursively. `mc support inspect ALIAS/bucket/path/to/file.txt/*/part.*` will collect parts for all versions for the object located at `bucket/path/to/file.txt`.
`xl-meta` accepts zip files as input and will output all `xl.meta` files found within the archive. For example:
```
$ mc admin inspect play/test123/test*/xl.meta
$ mc support inspect play/test123/test*/xl.meta
mc: File data successfully downloaded as inspect.6f96b336.zip
$ xl-meta inspect.6f96b336.zip
{
@@ -105,7 +105,7 @@ $ xl-meta inspect.6f96b336.zip
Optionally `--encrypt` can be specified. This will output an encrypted file and a decryption key:
```
$ mc admin inspect --encrypt play/test123/test*/*/part.*
$ mc support inspect --encrypt play/test123/test*/*/part.*
mc: Encrypted file data successfully downloaded as inspect.ad2b43d8.enc
mc: Decryption key: ad2b43d847fdb14e54c5836200177f7158b3f745433525f5d23c0e0208e50c9948540b54
+1 -1
View File
@@ -70,7 +70,7 @@ Minimum permissions required if you wish to provide restricted access with your
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject",
"s3:HeadBucket"
"s3:GetBucketAcl",
],
"Resource": [
"arn:aws:s3:::mybucket",

Some files were not shown because too many files have changed in this diff Show More