Skip to content

Commit f06402a

Browse files
Merge pull request #1784 from jakub-nowakowski-percona/PXB-3863-8.4
PXB-3863 [8.4]: Fix xbcloud with Azure Hierarchical Namespace (HNS) storage
2 parents 480a801 + e0b6d47 commit f06402a

5 files changed

Lines changed: 67 additions & 3 deletions

File tree

storage/innobase/xtrabackup/src/xbcloud/azure.cc

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,6 @@ void Azure_signer::sign_request(const std::string &container,
197197

198198
std::string decoded_access_key = base64_decode(access_key);
199199

200-
trim(decoded_access_key);
201-
202200
auto signature =
203201
base64_encode(hmac_sha256(decoded_access_key, string_to_sign));
204202

@@ -378,6 +376,32 @@ bool Azure_client::container_exists(const std::string &name, bool &exists) {
378376
return false;
379377
}
380378

379+
bool Azure_client::is_hns_enabled() {
380+
if (hns_enabled.has_value()) {
381+
return hns_enabled.value();
382+
}
383+
384+
Http_request req(Http_request::GET, protocol, host, "/");
385+
req.add_param("restype", "account");
386+
req.add_param("comp", "properties");
387+
signer->sign_request("", "", req, time(0));
388+
389+
Http_response resp;
390+
if (!http_client->make_request(req, resp)) {
391+
hns_enabled = false;
392+
return false;
393+
}
394+
395+
if (!resp.ok()) {
396+
hns_enabled = false;
397+
return false;
398+
}
399+
400+
auto it = resp.headers().find("x-ms-is-hns-enabled");
401+
hns_enabled = it != resp.headers().end() && it->second == "true";
402+
return hns_enabled.value();
403+
}
404+
381405
bool Azure_client::upload_object(const std::string &container,
382406
const std::string &name,
383407
const Http_buffer &contents) {

storage/innobase/xtrabackup/src/xbcloud/azure.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
2222
#define __XBCLOUD_AZURE_H__
2323

2424
#include <iostream>
25+
#include <optional>
2526
#include "object_store.h"
2627
#include "xbcloud/http.h"
2728
#include "xbcloud/util.h"
@@ -93,6 +94,8 @@ class Azure_client {
9394
ulong max_retries;
9495
ulong max_backoff;
9596

97+
std::optional<bool> hns_enabled;
98+
9699
static void upload_callback(Azure_client *client, std::string container,
97100
std::string name, Http_request *req,
98101
Http_response *resp,
@@ -171,6 +174,8 @@ class Azure_client {
171174
std::vector<std::string> &files,
172175
std::vector<std::string> &dirs);
173176

177+
bool is_hns_enabled();
178+
174179
ulong get_max_retries() { return max_retries; }
175180

176181
ulong get_max_backoff() { return max_backoff; }
@@ -266,6 +271,20 @@ class Azure_object_store : public Object_store {
266271
return azure_client.list_objects_files_and_dirs(container, directory + "/",
267272
files, dirs);
268273
}
274+
// Strip leading slashes for HNS-enabled accounts.
275+
// Azure HNS silently normalizes blob paths by removing leading slashes,
276+
// so "/backup/chunk.00000" is stored as "backup/chunk.00000". Without
277+
// this, xbcloud's expected prefix no longer matches what the API returns.
278+
// Example: "/db/mybackup" -> "db/mybackup"
279+
// Flat accounts are unaffected — the name is returned as-is.
280+
std::string normalize_name(const std::string &name) override {
281+
if (azure_client.is_hns_enabled()) {
282+
std::string result = name;
283+
ltrim_slashes(result);
284+
return result;
285+
}
286+
return name;
287+
}
269288
};
270289
} // namespace xbcloud
271290

storage/innobase/xtrabackup/src/xbcloud/object_store.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ class Object_store {
7171
const std::string &directory,
7272
std::vector<std::string> &files,
7373
std::vector<std::string> &dirs);
74+
/**
75+
* Normalize a backup name according to storage-specific rules.
76+
*
77+
* Default implementation returns the name unchanged.
78+
*
79+
* @param name Backup name to normalize.
80+
* @return Normalized name.
81+
*/
82+
virtual std::string normalize_name(const std::string &name) { return name; }
7483
virtual ~Object_store() {}
7584
};
7685

storage/innobase/xtrabackup/src/xbcloud/util.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ static inline void rtrim_slashes(std::string &s) {
6565
s.end());
6666
}
6767

68+
static inline void ltrim_slashes(std::string &s) {
69+
s.erase(s.begin(),
70+
std::find_if(s.begin(), s.end(), [](int ch) { return ch != '/'; }));
71+
}
72+
6873
static inline void to_lower(std::string &s) {
6974
s.resize(s.length());
7075
std::transform(s.begin(), s.end(), s.begin(), ::tolower);

storage/innobase/xtrabackup/src/xbcloud/xbcloud.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,7 @@ bool xbcloud_put(Object_store *store, const std::string &container,
10371037
@return true in case of success or false otherwise */
10381038
bool chunk_name_to_file_name(const std::string &chunk_name,
10391039
std::string &file_name, my_off_t &idx) {
1040-
if (chunk_name.size() < 22 && chunk_name[chunk_name.size() - 21] != '.') {
1040+
if (chunk_name.size() < 22 || chunk_name[chunk_name.size() - 21] != '.') {
10411041
/* chunk name is invalid */
10421042
return false;
10431043
}
@@ -1605,6 +1605,13 @@ int main(int argc, char **argv) {
16051605
reinterpret_cast<Azure_object_store *>(object_store.get())
16061606
->set_extra_http_headers(extra_http_headers);
16071607
}
1608+
1609+
backup_name = object_store->normalize_name(backup_name);
1610+
if (backup_name.empty()) {
1611+
msg_ts("%s: Backup name is empty after normalization.\n", my_progname);
1612+
return EXIT_FAILURE;
1613+
}
1614+
16081615
/* validation */
16091616
if (opt_threads > 1 && opt_fifo_dir == nullptr && opt_mode != MODE_DELETE) {
16101617
msg_ts(

0 commit comments

Comments
 (0)