Skip to content

Commit 323eb1f

Browse files
authored
Merge branch '4.20' into fix-vmware-ip-loss
2 parents 0dd48f1 + 0648d00 commit 323eb1f

File tree

37 files changed

+7022
-306
lines changed

37 files changed

+7022
-306
lines changed

core/src/main/java/com/cloud/storage/template/HttpTemplateDownloader.java

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.RandomAccessFile;
2828
import java.net.URI;
2929
import java.net.URISyntaxException;
30+
import java.util.Arrays;
3031
import java.util.Date;
3132
import java.util.List;
3233

@@ -80,6 +81,18 @@ public class HttpTemplateDownloader extends ManagedContextRunnable implements Te
8081
private ResourceType resourceType = ResourceType.TEMPLATE;
8182
private final HttpMethodRetryHandler myretryhandler;
8283
private boolean followRedirects = false;
84+
private boolean isChunkedTransfer;
85+
86+
protected static final List<String> CUSTOM_HEADERS_FOR_CHUNKED_TRANSFER_SIZE = Arrays.asList(
87+
"x-goog-stored-content-length",
88+
"x-goog-meta-size",
89+
"x-amz-meta-size",
90+
"x-amz-meta-content-length",
91+
"x-object-meta-size",
92+
"x-original-content-length",
93+
"x-oss-meta-content-length",
94+
"x-file-size");
95+
private static final long MIN_FORMAT_VERIFICATION_SIZE = 1024 * 1024;
8396

8497
public HttpTemplateDownloader(StorageLayer storageLayer, String downloadUrl, String toDir, DownloadCompleteCallback callback, long maxTemplateSizeInBytes,
8598
String user, String password, Proxy proxy, ResourceType resourceType) {
@@ -205,13 +218,11 @@ public long download(boolean resume, DownloadCompleteCallback callback) {
205218
RandomAccessFile out = new RandomAccessFile(file, "rw");
206219
) {
207220
out.seek(localFileSize);
208-
209-
logger.info("Starting download from " + downloadUrl + " to " + toFile + " remoteSize=" + toHumanReadableSize(remoteSize) + " , max size=" + toHumanReadableSize(maxTemplateSizeInBytes));
210-
211-
if (copyBytes(file, in, out)) return 0;
212-
221+
logger.info("Starting download from {} to {} remoteSize={} , max size={}",downloadUrl, toFile,
222+
toHumanReadableSize(remoteSize), toHumanReadableSize(maxTemplateSizeInBytes));
223+
boolean eof = copyBytes(file, in, out);
213224
Date finish = new Date();
214-
checkDowloadCompletion();
225+
checkDownloadCompletion(eof);
215226
downloadTime += finish.getTime() - start.getTime();
216227
} finally { /* in.close() and out.close() */ }
217228
return totalBytes;
@@ -237,28 +248,32 @@ public long download(boolean resume, DownloadCompleteCallback callback) {
237248
}
238249

239250
private boolean copyBytes(File file, InputStream in, RandomAccessFile out) throws IOException {
240-
int bytes;
241-
byte[] block = new byte[CHUNK_SIZE];
251+
byte[] buffer = new byte[CHUNK_SIZE];
242252
long offset = 0;
243-
boolean done = false;
244253
VerifyFormat verifyFormat = new VerifyFormat(file);
245254
status = Status.IN_PROGRESS;
246-
while (!done && status != Status.ABORTED && offset <= remoteSize) {
247-
if ((bytes = in.read(block, 0, CHUNK_SIZE)) > -1) {
248-
offset = writeBlock(bytes, out, block, offset);
249-
if (!ResourceType.SNAPSHOT.equals(resourceType) &&
250-
!verifyFormat.isVerifiedFormat() &&
251-
(offset >= 1048576 || offset >= remoteSize)) { //let's check format after we get 1MB or full file
252-
verifyFormat.invoke();
253-
}
254-
} else {
255-
done = true;
255+
while (status != Status.ABORTED) {
256+
int bytesRead = in.read(buffer, 0, CHUNK_SIZE);
257+
if (bytesRead == -1) {
258+
logger.debug("Reached EOF on input stream");
259+
break;
260+
}
261+
offset = writeBlock(bytesRead, out, buffer, offset);
262+
if (!ResourceType.SNAPSHOT.equals(resourceType)
263+
&& !verifyFormat.isVerifiedFormat()
264+
&& (offset >= MIN_FORMAT_VERIFICATION_SIZE || offset >= remoteSize)) {
265+
verifyFormat.invoke();
266+
}
267+
if (offset >= remoteSize) {
268+
logger.debug("Reached expected remote size limit: {} bytes", remoteSize);
269+
break;
256270
}
257271
}
258272
out.getFD().sync();
259-
return false;
273+
return !Status.ABORTED.equals(status);
260274
}
261275

276+
262277
private long writeBlock(int bytes, RandomAccessFile out, byte[] block, long offset) throws IOException {
263278
out.write(block, 0, bytes);
264279
offset += bytes;
@@ -267,11 +282,13 @@ private long writeBlock(int bytes, RandomAccessFile out, byte[] block, long offs
267282
return offset;
268283
}
269284

270-
private void checkDowloadCompletion() {
285+
private void checkDownloadCompletion(boolean eof) {
271286
String downloaded = "(incomplete download)";
272-
if (totalBytes >= remoteSize) {
287+
if (eof && ((totalBytes >= remoteSize) || (isChunkedTransfer && remoteSize == maxTemplateSizeInBytes))) {
273288
status = Status.DOWNLOAD_FINISHED;
274-
downloaded = "(download complete remote=" + toHumanReadableSize(remoteSize) + " bytes)";
289+
downloaded = "(download complete remote=" +
290+
(remoteSize == maxTemplateSizeInBytes ? toHumanReadableSize(remoteSize) : "unknown") +
291+
" bytes)";
275292
}
276293
errorString = "Downloaded " + toHumanReadableSize(totalBytes) + " bytes " + downloaded;
277294
}
@@ -293,18 +310,42 @@ private void checkAndSetDownloadSize() {
293310
}
294311
}
295312

313+
protected long getRemoteSizeForChunkedTransfer() {
314+
for (String headerKey : CUSTOM_HEADERS_FOR_CHUNKED_TRANSFER_SIZE) {
315+
Header header = request.getResponseHeader(headerKey);
316+
if (header == null) {
317+
continue;
318+
}
319+
try {
320+
return Long.parseLong(header.getValue());
321+
} catch (NumberFormatException ignored) {}
322+
}
323+
Header contentRangeHeader = request.getResponseHeader("Content-Range");
324+
if (contentRangeHeader != null) {
325+
String contentRange = contentRangeHeader.getValue();
326+
if (contentRange != null && contentRange.contains("/")) {
327+
String totalSize = contentRange.substring(contentRange.indexOf('/') + 1).trim();
328+
return Long.parseLong(totalSize);
329+
}
330+
}
331+
return 0;
332+
}
333+
296334
private boolean tryAndGetRemoteSize() {
297335
Header contentLengthHeader = request.getResponseHeader("content-length");
298-
boolean chunked = false;
336+
isChunkedTransfer = false;
299337
long reportedRemoteSize = 0;
300338
if (contentLengthHeader == null) {
301339
Header chunkedHeader = request.getResponseHeader("Transfer-Encoding");
302-
if (chunkedHeader == null || !"chunked".equalsIgnoreCase(chunkedHeader.getValue())) {
340+
if (chunkedHeader != null && "chunked".equalsIgnoreCase(chunkedHeader.getValue())) {
341+
isChunkedTransfer = true;
342+
reportedRemoteSize = getRemoteSizeForChunkedTransfer();
343+
logger.debug("{} is using chunked transfer encoding, possible remote size: {}", downloadUrl,
344+
reportedRemoteSize);
345+
} else {
303346
status = Status.UNRECOVERABLE_ERROR;
304347
errorString = " Failed to receive length of download ";
305348
return false;
306-
} else if ("chunked".equalsIgnoreCase(chunkedHeader.getValue())) {
307-
chunked = true;
308349
}
309350
} else {
310351
reportedRemoteSize = Long.parseLong(contentLengthHeader.getValue());
@@ -316,9 +357,11 @@ private boolean tryAndGetRemoteSize() {
316357
return false;
317358
}
318359
}
319-
320360
if (remoteSize == 0) {
321361
remoteSize = reportedRemoteSize;
362+
if (remoteSize != 0) {
363+
logger.debug("Remote size for {} found to be {}", downloadUrl, toHumanReadableSize(remoteSize));
364+
}
322365
}
323366
return true;
324367
}

engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,59 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.storage_pool', 'used_iops', 'bigint
5454
-- Add reason column for op_ha_work
5555
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.op_ha_work', 'reason', 'varchar(32) DEFAULT NULL COMMENT "Reason for the HA work"');
5656

57+
-- Support for XCP-ng 8.3.0 and XenServer 8.4 by adding hypervisor capabilities
58+
-- https://docs.xenserver.com/en-us/xenserver/8/system-requirements/configuration-limits.html
59+
-- https://docs.xenserver.com/en-us/citrix-hypervisor/system-requirements/configuration-limits.html
60+
INSERT IGNORE INTO `cloud`.`hypervisor_capabilities`(uuid, hypervisor_type, hypervisor_version, max_guests_limit, max_data_volumes_limit, max_hosts_per_cluster, storage_motion_supported) VALUES (UUID(), 'XenServer', '8.3.0', 1000, 254, 64, 1);
61+
INSERT IGNORE INTO `cloud`.`hypervisor_capabilities`(uuid, hypervisor_type, hypervisor_version, max_guests_limit, max_data_volumes_limit, max_hosts_per_cluster, storage_motion_supported) VALUES (UUID(), 'XenServer', '8.4.0', 1000, 240, 64, 1);
62+
63+
-- Add missing and new Guest OS mappings
64+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Debian GNU/Linux 10 (64-bit)', 'XenServer', '8.2.1', 'Debian Buster 10');
65+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (5, 'SUSE Linux Enterprise Server 15 (64-bit)', 'XenServer', '8.2.1', 'SUSE Linux Enterprise 15 (64-bit)');
66+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2022 (64-bit)', 'XenServer', '8.2.1', 'Windows Server 2022 (64-bit)');
67+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows 11 (64-bit)', 'XenServer', '8.2.1', 'Windows 11');
68+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (10, 'Ubuntu 20.04 LTS', 'XenServer', '8.2.1', 'Ubuntu Focal Fossa 20.04');
69+
70+
-- Copy XS 8.2.1 hypervisor guest OS mappings to XS 8.3 and 8.3 mappings to 8.4
71+
INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) SELECT UUID(),'Xenserver', '8.3.0', guest_os_name, guest_os_id, utc_timestamp(), 0 FROM `cloud`.`guest_os_hypervisor` WHERE hypervisor_type='Xenserver' AND hypervisor_version='8.2.1';
72+
73+
-- Add new and missing guest os mappings for XS 8.3
74+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'Rocky Linux 9', 'XenServer', '8.3.0', 'Rocky Linux 9');
75+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'Rocky Linux 8', 'XenServer', '8.3.0', 'Rocky Linux 8');
76+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'AlmaLinux 9', 'XenServer', '8.3.0', 'AlmaLinux 9');
77+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'AlmaLinux 8', 'XenServer', '8.3.0', 'AlmaLinux 8');
78+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Debian GNU/Linux 12 (64-bit)', 'XenServer', '8.3.0', 'Debian Bookworm 12');
79+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (3, 'Oracle Linux 9', 'XenServer', '8.3.0', 'Oracle Linux 9');
80+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (3, 'Oracle Linux 8', 'XenServer', '8.3.0', 'Oracle Linux 8');
81+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (4, 'Red Hat Enterprise Linux 8.0', 'XenServer', '8.3.0', 'Red Hat Enterprise Linux 8');
82+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (4, 'Red Hat Enterprise Linux 9.0', 'XenServer', '8.3.0', 'Red Hat Enterprise Linux 9');
83+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (10, 'Ubuntu 22.04 LTS', 'XenServer', '8.3.0', 'Ubuntu Jammy Jellyfish 22.04');
84+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (5, 'SUSE Linux Enterprise Server 12 SP5 (64-bit)', 'XenServer', '8.3.0', 'SUSE Linux Enterprise Server 12 SP5 (64-bit');
85+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (4, 'NeoKylin Linux Server 7', 'XenServer', '8.3.0', 'NeoKylin Linux Server 7');
86+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'CentOS Stream 9', 'XenServer', '8.3.0', 'CentOS Stream 9');
87+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (4, 'Scientific Linux 7', 'XenServer', '8.3.0', 'Scientific Linux 7');
88+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (7, 'Generic Linux UEFI', 'XenServer', '8.3.0', 'Generic Linux UEFI');
89+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (7, 'Generic Linux BIOS', 'XenServer', '8.3.0', 'Generic Linux BIOS');
90+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Gooroom Platform 2.0', 'XenServer', '8.3.0', 'Gooroom Platform 2.0');
91+
92+
INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid,hypervisor_type, hypervisor_version, guest_os_name, guest_os_id, created, is_user_defined) SELECT UUID(),'Xenserver', '8.4.0', guest_os_name, guest_os_id, utc_timestamp(), 0 FROM `cloud`.`guest_os_hypervisor` WHERE hypervisor_type='Xenserver' AND hypervisor_version='8.3.0';
93+
94+
-- Add new guest os mappings for XS 8.4 and KVM
95+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025', 'XenServer', '8.4.0', 'Windows Server 2025');
96+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (10, 'Ubuntu 24.04 LTS', 'XenServer', '8.4.0', 'Ubuntu Noble Numbat 24.04');
97+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Debian GNU/Linux 10 (64-bit)', 'KVM', 'default', 'Debian GNU/Linux 10 (64-bit)');
98+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Debian GNU/Linux 11 (64-bit)', 'KVM', 'default', 'Debian GNU/Linux 11 (64-bit)');
99+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Debian GNU/Linux 12 (64-bit)', 'KVM', 'default', 'Debian GNU/Linux 12 (64-bit)');
100+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows 11 (64-bit)', 'KVM', 'default', 'Windows 11');
101+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (6, 'Windows Server 2025', 'KVM', 'default', 'Windows Server 2025');
102+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (10, 'Ubuntu 24.04 LTS', 'KVM', 'default', 'Ubuntu 24.04 LTS');
103+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'CentOS Stream 10 (preview)', 'XenServer', '8.4.0', 'CentOS Stream 10 (preview)');
104+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (1, 'CentOS Stream 9', 'XenServer', '8.4.0', 'CentOS Stream 9');
105+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (4, 'Scientific Linux 7', 'XenServer', '8.4.0', 'Scientific Linux 7');
106+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (4, 'NeoKylin Linux Server 7', 'XenServer', '8.4.0', 'NeoKylin Linux Server 7');
107+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (5, 'SUSE Linux Enterprise Server 12 SP5 (64-bit)', 'XenServer', '8.4.0', 'SUSE Linux Enterprise Server 12 SP5 (64-bit');
108+
CALL ADD_GUEST_OS_AND_HYPERVISOR_MAPPING (2, 'Gooroom Platform 2.0', 'XenServer', '8.4.0', 'Gooroom Platform 2.0');
109+
57110
-- Grant access to 2FA APIs for the "Read-Only User - Default" role
58111

59112
CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Read-Only User - Default', 'setupUserTwoFactorAuthentication', 'ALLOW');

engine/storage/configdrive/src/main/java/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,26 @@ public static String buildConfigDrive(List<NicProfile> nics, List<String[]> vmDa
126126

127127
File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName);
128128

129-
writeVendorEmptyJsonFile(openStackFolder);
129+
/*
130+
Try to find VM password in the vmData.
131+
If it is found, then write it into vendor-data.json
132+
*/
133+
String vmPassword = "";
134+
for (String[] item : vmData) {
135+
String dataType = item[CONFIGDATA_DIR];
136+
String fileName = item[CONFIGDATA_FILE];
137+
String content = item[CONFIGDATA_CONTENT];
138+
if (PASSWORD_FILE.equals(fileName)) {
139+
vmPassword = content;
140+
break;
141+
}
142+
}
143+
if (vmPassword.equals("")) {
144+
writeVendorDataJsonFile(openStackFolder);
145+
} else {
146+
writeVendorDataJsonFile(openStackFolder, vmPassword);
147+
}
148+
130149
writeNetworkData(nics, supportedServices, openStackFolder);
131150
for (NicProfile nic: nics) {
132151
if (supportedServices.get(nic.getId()).contains(Network.Service.UserData)) {
@@ -253,14 +272,34 @@ static boolean needForGeneratingNetworkData(Map<Long, List<Network.Service>> sup
253272
*
254273
* If the folder does not exist, and we cannot create it, we throw a {@link CloudRuntimeException}.
255274
*/
256-
static void writeVendorEmptyJsonFile(File openStackFolder) {
275+
static void writeVendorDataJsonFile(File openStackFolder) {
257276
if (openStackFolder.exists() || openStackFolder.mkdirs()) {
258277
writeFile(openStackFolder, "vendor_data.json", "{}");
259278
} else {
260279
throw new CloudRuntimeException("Failed to create folder " + openStackFolder);
261280
}
262281
}
263282

283+
/**
284+
* Writes vendor data containing Cloudstack-generated password into vendor-data.json
285+
*
286+
* If the folder does not exist, and we cannot create it, we throw a {@link CloudRuntimeException}.
287+
*/
288+
static void writeVendorDataJsonFile(File openStackFolder, String password) {
289+
if (openStackFolder.exists() || openStackFolder.mkdirs()) {
290+
writeFile(
291+
openStackFolder,
292+
"vendor_data.json",
293+
String.format(
294+
"{\"cloud-init\": \"#cloud-config\\npassword: %s\\nchpasswd:\\n expire: False\"}",
295+
password
296+
)
297+
);
298+
} else {
299+
throw new CloudRuntimeException("Failed to create folder " + openStackFolder);
300+
}
301+
}
302+
264303
/**
265304
* Creates the {@link JsonObject} with VM's metadata. The vmData is a list of arrays; we expect this list to have the following entries:
266305
* <ul>

0 commit comments

Comments
 (0)