From 49a862b223ea4bf41b793ef5a4a9da95ad1b1d59 Mon Sep 17 00:00:00 2001 From: rashmidixit Date: Tue, 28 Feb 2017 12:03:07 +0530 Subject: [PATCH] CLOUDSTACK-9700: Allow user to Register/Copy templates to multiple zones at the same time --- .../apache/cloudstack/api/ApiConstants.java | 2 + .../cloudstack/api/ResponseGenerator.java | 6 +- .../template/CopyTemplateCmdByAdmin.java | 11 +- .../template/RegisterTemplateCmdByAdmin.java | 15 +- .../user/template/CopyTemplateCmd.java | 48 +++- .../user/template/RegisterTemplateCmd.java | 52 +++- .../template/CopyTemplateCmdByAdminTest.java | 77 ++++++ .../user/template/CopyTemplateCmdTest.java | 75 ++++++ .../RegisterTemplateCmdByAdminTest.java | 113 +++++++++ .../template/RegisterTemplateCmdTest.java | 111 ++++++++ .../manager/BareMetalTemplateAdapter.java | 33 ++- .../src/com/cloud/api/ApiResponseHelper.java | 17 ++ .../com/cloud/storage/TemplateProfile.java | 26 +- .../template/HypervisorTemplateAdapter.java | 78 ++++-- .../com/cloud/template/TemplateAdapter.java | 4 +- .../cloud/template/TemplateAdapterBase.java | 59 +++-- .../cloud/template/TemplateManagerImpl.java | 129 +++++++--- .../HypervisorTemplateAdapterTest.java | 2 +- ui/css/cloudstack3.css | 30 ++- ui/index.html | 1 + ui/l10n/en.js | 1 + ui/scripts/docs.js | 4 +- ui/scripts/sharedFunctions.js | 10 + ui/scripts/templates.js | 236 +++++++++++------- ui/scripts/ui-custom/copyTemplate.js | 130 ++++++++++ ui/scripts/ui/dialog.js | 19 +- 26 files changed, 1060 insertions(+), 229 deletions(-) create mode 100644 api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdByAdminTest.java create mode 100644 api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdTest.java create mode 100644 api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdByAdminTest.java create mode 100644 api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java create mode 100644 ui/scripts/ui-custom/copyTemplate.js diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 51f5e493acd..5784c9d0bc2 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -652,6 +652,8 @@ public class ApiConstants { public static final String OVM3_VIP = "ovm3vip"; public static final String CLEAN_UP_DETAILS = "cleanupdetails"; + public static final String ZONE_ID_LIST = "zoneids"; + public static final String DESTINATION_ZONE_ID_LIST = "destzoneids"; public static final String ADMIN = "admin"; public enum HostDetails { diff --git a/api/src/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/org/apache/cloudstack/api/ResponseGenerator.java index 8da1dedcee5..9fd4d847344 100644 --- a/api/src/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/org/apache/cloudstack/api/ResponseGenerator.java @@ -307,7 +307,11 @@ public interface ResponseGenerator { TemplateResponse createTemplateUpdateResponse(ResponseView view, VirtualMachineTemplate result); - List createTemplateResponses(ResponseView view, VirtualMachineTemplate result, Long zoneId, boolean readyOnly); + List createTemplateResponses(ResponseView view, VirtualMachineTemplate result, + Long zoneId, boolean readyOnly); + + List createTemplateResponses(ResponseView view, VirtualMachineTemplate result, + List zoneIds, boolean readyOnly); List createCapacityResponse(List result, DecimalFormat format); diff --git a/api/src/org/apache/cloudstack/api/command/admin/template/CopyTemplateCmdByAdmin.java b/api/src/org/apache/cloudstack/api/command/admin/template/CopyTemplateCmdByAdmin.java index 11c00e3a4b7..e0c798c7849 100644 --- a/api/src/org/apache/cloudstack/api/command/admin/template/CopyTemplateCmdByAdmin.java +++ b/api/src/org/apache/cloudstack/api/command/admin/template/CopyTemplateCmdByAdmin.java @@ -40,11 +40,20 @@ public class CopyTemplateCmdByAdmin extends CopyTemplateCmd { @Override public void execute() throws ResourceAllocationException{ try { + if (destZoneId == null && (destZoneIds == null || destZoneIds.size() == 0)) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Either destzoneid or destzoneids parameters have to be specified."); + + if (destZoneId != null && destZoneIds != null && destZoneIds.size() != 0) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Both destzoneid and destzoneids cannot be specified at the same time."); + CallContext.current().setEventDetails(getEventDescription()); VirtualMachineTemplate template = _templateService.copyTemplate(this); if (template != null){ - List listResponse = _responseGenerator.createTemplateResponses(ResponseView.Full, template, getDestinationZoneId(), false); + List listResponse = _responseGenerator.createTemplateResponses(ResponseView.Full, template, + getDestinationZoneIds(), false); TemplateResponse response = new TemplateResponse(); if (listResponse != null && !listResponse.isEmpty()) { response = listResponse.get(0); diff --git a/api/src/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java b/api/src/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java index 68d53f9df6f..7281197305b 100644 --- a/api/src/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java +++ b/api/src/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java @@ -40,10 +40,23 @@ public class RegisterTemplateCmdByAdmin extends RegisterTemplateCmd { @Override public void execute() throws ResourceAllocationException{ try { + if ((zoneId != null) && (zoneIds != null && !zoneIds.isEmpty())) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Both zoneid and zoneids cannot be specified at the same time"); + + if (zoneId == null && (zoneIds == null || zoneIds.isEmpty())) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Either zoneid or zoneids is required. Both cannot be null."); + + if (zoneIds != null && zoneIds.size() > 1 && zoneIds.contains(-1L)) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Parameter zoneids cannot combine all zones (-1) option with other zones"); + VirtualMachineTemplate template = _templateService.registerTemplate(this); if (template != null){ ListResponse response = new ListResponse(); - List templateResponses = _responseGenerator.createTemplateResponses(ResponseView.Full, template, zoneId, false); + List templateResponses = _responseGenerator.createTemplateResponses(ResponseView.Full, template, + zoneIds, false); response.setResponses(templateResponses); response.setResponseName(getCommandName()); setResponseObject(response); diff --git a/api/src/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java b/api/src/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java index fdbdafd1446..d16b87cd95e 100644 --- a/api/src/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/template/CopyTemplateCmd.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.api.command.user.template; +import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; @@ -51,25 +52,46 @@ public class CopyTemplateCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.DESTINATION_ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, - required = true, + required = false, description = "ID of the zone the template is being copied to.") - private Long destZoneId; + protected Long destZoneId; - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class, required = true, description = "Template ID.") + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, + entityType = TemplateResponse.class, required = true, description = "Template ID.") private Long id; @Parameter(name = ApiConstants.SOURCE_ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, - description = "ID of the zone the template is currently hosted on. If not specified and template is cross-zone, then we will sync this template to region wide image store.") + description = "ID of the zone the template is currently hosted on. " + + "If not specified and template is cross-zone, " + + "then we will sync this template to region wide image store.") private Long sourceZoneId; + @Parameter(name = ApiConstants.DESTINATION_ZONE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = ZoneResponse.class, + required = false, + description = "A list of IDs of the zones that the template needs to be copied to." + + "Specify this list if the template needs to copied to multiple zones in one go. " + + "Do not specify destzoneid and destzoneids together, however one of them is required.") + protected List destZoneIds; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// - public Long getDestinationZoneId() { - return destZoneId; + public List getDestinationZoneIds() { + if (destZoneIds != null && destZoneIds.size() != 0) { + return destZoneIds; + } + if (destZoneId != null) { + List < Long > destIds = new ArrayList<>(); + destIds.add(destZoneId); + return destIds; + } + return null; } public Long getId() { @@ -111,7 +133,8 @@ public class CopyTemplateCmd extends BaseAsyncCmd { @Override public String getEventDescription() { - return "copying template: " + getId() + " from zone: " + getSourceZoneId() + " to zone: " + getDestinationZoneId(); + return "copying template: " + getId() + " from zone: " + getSourceZoneId() + + " to zone: " + getDestinationZoneIds(); } @Override @@ -127,11 +150,20 @@ public class CopyTemplateCmd extends BaseAsyncCmd { @Override public void execute() throws ResourceAllocationException { try { + if (destZoneId == null && (destZoneIds == null || destZoneIds.size() == 0)) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Either destzoneid or destzoneids parameters have to be specified."); + + if (destZoneId != null && destZoneIds != null && destZoneIds.size() != 0) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Both destzoneid and destzoneids cannot be specified at the same time."); + CallContext.current().setEventDetails(getEventDescription()); VirtualMachineTemplate template = _templateService.copyTemplate(this); if (template != null){ - List listResponse = _responseGenerator.createTemplateResponses(ResponseView.Restricted, template, getDestinationZoneId(), false); + List listResponse = _responseGenerator.createTemplateResponses(ResponseView.Restricted, + template, getDestinationZoneIds(), false); TemplateResponse response = new TemplateResponse(); if (listResponse != null && !listResponse.isEmpty()) { response = listResponse.get(0); diff --git a/api/src/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java b/api/src/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java index 8ff0b6b449e..aff9d464a61 100644 --- a/api/src/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.template; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -109,7 +110,7 @@ public class RegisterTemplateCmd extends BaseCmd { private String url; @Parameter(name=ApiConstants.ZONE_ID, type=CommandType.UUID, entityType = ZoneResponse.class, - required=true, description="the ID of the zone the template is to be hosted on") + required=false, description="the ID of the zone the template is to be hosted on") protected Long zoneId; @Parameter(name = ApiConstants.DOMAIN_ID, @@ -130,7 +131,8 @@ public class RegisterTemplateCmd extends BaseCmd { @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Register template for the project") private Long projectId; - @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Template details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].hypervisortoolsversion=xenserver61") + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, + description = "Template details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].hypervisortoolsversion=xenserver61") protected Map details; @Parameter(name = ApiConstants.IS_DYNAMICALLY_SCALABLE, @@ -141,6 +143,19 @@ public class RegisterTemplateCmd extends BaseCmd { @Parameter(name = ApiConstants.ROUTING, type = CommandType.BOOLEAN, description = "true if the template type is routing i.e., if template is used to deploy router") protected Boolean isRoutingType; + @Parameter(name=ApiConstants.ZONE_ID_LIST, + type=CommandType.LIST, + collectionType = CommandType.UUID, + entityType = ZoneResponse.class, + required=false, + since="4.10.0.0", + description="A list of zone ids where the template will be hosted. Use this parameter if the template needs " + + "to be registered to multiple zones in one go. Use zoneid if the template " + + "needs to be registered to only one zone." + + "Passing only -1 to this will cause the template to be registered as a cross " + + "zone template and will be copied to all zones. ") + protected List zoneIds; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -197,8 +212,22 @@ public class RegisterTemplateCmd extends BaseCmd { return url; } - public Long getZoneId() { - return zoneId; + public List getZoneIds() { + // This function will return null when the zoneId + //is -1 which means all zones. + if (zoneIds != null && !(zoneIds.isEmpty())) { + if ((zoneIds.size() == 1) && (zoneIds.get(0) == -1L)) + return null; + else + return zoneIds; + } + if (zoneId == null) + return null; + if (zoneId!= null && zoneId == -1) + return null; + List zones = new ArrayList<>(); + zones.add(zoneId); + return zones; } public Long getDomainId() { @@ -261,10 +290,23 @@ public class RegisterTemplateCmd extends BaseCmd { @Override public void execute() throws ResourceAllocationException { try { + if ((zoneId != null) && (zoneIds != null && !zoneIds.isEmpty())) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Both zoneid and zoneids cannot be specified at the same time"); + + if (zoneId == null && (zoneIds == null || zoneIds.isEmpty())) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Either zoneid or zoneids is required. Both cannot be null."); + + if (zoneIds != null && zoneIds.size() > 1 && zoneIds.contains(-1L)) + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, + "Parameter zoneids cannot combine all zones (-1) option with other zones"); + VirtualMachineTemplate template = _templateService.registerTemplate(this); if (template != null) { ListResponse response = new ListResponse(); - List templateResponses = _responseGenerator.createTemplateResponses(ResponseView.Restricted, template, zoneId, false); + List templateResponses = _responseGenerator.createTemplateResponses(ResponseView.Restricted, + template, getZoneIds(), false); response.setResponses(templateResponses); response.setResponseName(getCommandName()); setResponseObject(response); diff --git a/api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdByAdminTest.java b/api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdByAdminTest.java new file mode 100644 index 00000000000..d34c31ffaf2 --- /dev/null +++ b/api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdByAdminTest.java @@ -0,0 +1,77 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + +package org.apache.cloudstack.api.command.user.template; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.template.TemplateApiService; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.template.CopyTemplateCmdByAdmin; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.ArrayList; + +/** + * Created by stack on 7/21/16. + */ +@RunWith(MockitoJUnitRunner.class) +public class CopyTemplateCmdByAdminTest{ + + @InjectMocks + private CopyTemplateCmdByAdmin copyTemplateCmdByAdmin; + + @Mock + public TemplateApiService _templateService; + + @Test + public void testZoneidAndZoneIdListEmpty() throws ResourceAllocationException { + try { + copyTemplateCmdByAdmin = new CopyTemplateCmdByAdmin(); + copyTemplateCmdByAdmin.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("API should fail when no parameters are passed."); + } + } + } + + @Test + public void testDestZoneidAndDestZoneIdListBothPresent() throws ResourceAllocationException { + try { + copyTemplateCmdByAdmin = new CopyTemplateCmdByAdmin(); + copyTemplateCmdByAdmin.destZoneId = -1L; + copyTemplateCmdByAdmin.destZoneIds = new ArrayList<>(); + copyTemplateCmdByAdmin.destZoneIds.add(-1L); + + copyTemplateCmdByAdmin.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Api should fail when both destzoneid and destzoneids are passed"); + } + } + } + + +} + diff --git a/api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdTest.java b/api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdTest.java new file mode 100644 index 00000000000..145aaa68d83 --- /dev/null +++ b/api/test/org/apache/cloudstack/api/command/user/template/CopyTemplateCmdTest.java @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.template; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.template.TemplateApiService; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.ArrayList; + +/** + * Created by stack on 7/21/16. + */ +@RunWith(MockitoJUnitRunner.class) +public class CopyTemplateCmdTest{ + + @InjectMocks + private CopyTemplateCmd copyTemplateCmd; + + @Mock + public TemplateApiService _templateService; + + @Test + public void testZoneidAndZoneIdListEmpty() throws ResourceAllocationException { + try { + copyTemplateCmd = new CopyTemplateCmd(); + copyTemplateCmd.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("API should fail when no parameters are passed."); + } + } + } + + @Test + public void testDestZoneidAndDestZoneIdListBothPresent() throws ResourceAllocationException { + try { + copyTemplateCmd = new CopyTemplateCmd(); + copyTemplateCmd.destZoneId = -1L; + copyTemplateCmd.destZoneIds = new ArrayList<>(); + copyTemplateCmd.destZoneIds.add(-1L); + + copyTemplateCmd.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Api should fail when both destzoneid and destzoneids are passed"); + } + } + } + + +} + diff --git a/api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdByAdminTest.java b/api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdByAdminTest.java new file mode 100644 index 00000000000..1ba7963b2b9 --- /dev/null +++ b/api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdByAdminTest.java @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + +package org.apache.cloudstack.api.command.user.template; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.template.TemplateApiService; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.template.RegisterTemplateCmdByAdmin; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import java.util.ArrayList; + +@RunWith(MockitoJUnitRunner.class) +public class RegisterTemplateCmdByAdminTest{ + + @InjectMocks + private RegisterTemplateCmdByAdmin registerTemplateCmdByAdmin; + + @Mock + public TemplateApiService _templateService; + + @Test + public void testZoneidAndZoneIdListEmpty() throws ResourceAllocationException { + try { + registerTemplateCmdByAdmin = new RegisterTemplateCmdByAdmin(); + registerTemplateCmdByAdmin.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Api should fail when both zoneid and zoneids aren't passed"); + } + } + } + + @Test + public void testZoneidAndZoneIdListBothPresent() throws ResourceAllocationException { + try { + registerTemplateCmdByAdmin = new RegisterTemplateCmdByAdmin(); + registerTemplateCmdByAdmin.zoneId = -1L; + registerTemplateCmdByAdmin.zoneIds = new ArrayList<>(); + registerTemplateCmdByAdmin.zoneIds.add(-1L); + + registerTemplateCmdByAdmin.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Api should fail when both zoneid and zoneids are passed"); + } + } + } + + + @Test + public void testZoneidMinusOne() throws ResourceAllocationException { + // If zoneId is passed as -1, then zone ids list should be null. + registerTemplateCmdByAdmin = new RegisterTemplateCmdByAdmin(); + registerTemplateCmdByAdmin.zoneId = -1L; + + Assert.assertNull(registerTemplateCmdByAdmin.getZoneIds()); + } + + @Test + public void testZoneidListMinusOne() throws ResourceAllocationException { + // If zoneId List has only one parameter -1, then zone ids list should be null. + registerTemplateCmdByAdmin = new RegisterTemplateCmdByAdmin(); + registerTemplateCmdByAdmin.zoneIds = new ArrayList<>(); + registerTemplateCmdByAdmin.zoneIds.add(-1L); + + Assert.assertNull(registerTemplateCmdByAdmin.getZoneIds()); + } + + @Test + public void testZoneidListMoreThanMinusOne() throws ResourceAllocationException { + try { + registerTemplateCmdByAdmin = new RegisterTemplateCmdByAdmin(); + registerTemplateCmdByAdmin.zoneIds = new ArrayList<>(); + registerTemplateCmdByAdmin.zoneIds.add(-1L); + registerTemplateCmdByAdmin.zoneIds.add(1L); + registerTemplateCmdByAdmin.execute(); + } catch (ServerApiException e) { + if (e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Parameter zoneids cannot combine all zones (-1) option with other zones"); + } + } + } + @Test + public void testZoneidPresentZoneidListAbsent() throws ResourceAllocationException { + registerTemplateCmdByAdmin = new RegisterTemplateCmdByAdmin(); + registerTemplateCmdByAdmin.zoneIds = null; + registerTemplateCmdByAdmin.zoneId = 1L; + Assert.assertEquals((Long)1L,registerTemplateCmdByAdmin.getZoneIds().get(0)); + } +} diff --git a/api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java b/api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java new file mode 100644 index 00000000000..f0cf6a91af2 --- /dev/null +++ b/api/test/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmdTest.java @@ -0,0 +1,111 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + + +package org.apache.cloudstack.api.command.user.template; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.template.TemplateApiService; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import java.util.ArrayList; + +@RunWith(MockitoJUnitRunner.class) +public class RegisterTemplateCmdTest{ + + @InjectMocks + private RegisterTemplateCmd registerTemplateCmd; + + @Mock + public TemplateApiService _templateService; + + @Test + public void testZoneidAndZoneIdListEmpty() throws ResourceAllocationException { + try { + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Api should fail when both zoneid and zoneids aren't passed"); + } + } + } + + @Test + public void testZoneidAndZoneIdListBothPresent() throws ResourceAllocationException { + try { + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.zoneId = -1L; + registerTemplateCmd.zoneIds = new ArrayList<>(); + registerTemplateCmd.zoneIds.add(-1L); + + registerTemplateCmd.execute(); + } catch (ServerApiException e) { + if(e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Api should fail when both zoneid and zoneids are passed"); + } + } + } + + + @Test + public void testZoneidMinusOne() throws ResourceAllocationException { + // If zoneId is passed as -1, then zone ids list should be null. + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.zoneId = -1L; + + Assert.assertNull(registerTemplateCmd.getZoneIds()); + } + + @Test + public void testZoneidListMinusOne() throws ResourceAllocationException { + // If zoneId List has only one parameter -1, then zone ids list should be null. + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.zoneIds = new ArrayList<>(); + registerTemplateCmd.zoneIds.add(-1L); + + Assert.assertNull(registerTemplateCmd.getZoneIds()); + } + @Test + public void testZoneidListMoreThanMinusOne() throws ResourceAllocationException { + try { + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.zoneIds = new ArrayList<>(); + registerTemplateCmd.zoneIds.add(-1L); + registerTemplateCmd.zoneIds.add(1L); + registerTemplateCmd.execute(); + } catch (ServerApiException e) { + if (e.getErrorCode() != ApiErrorCode.PARAM_ERROR) { + Assert.fail("Parameter zoneids cannot combine all zones (-1) option with other zones"); + } + } + } + @Test + public void testZoneidPresentZoneidListAbsent() throws ResourceAllocationException { + registerTemplateCmd = new RegisterTemplateCmd(); + registerTemplateCmd.zoneIds = null; + registerTemplateCmd.zoneId = 1L; + Assert.assertEquals((Long)1L,registerTemplateCmd.getZoneIds().get(0)); + } +} diff --git a/plugins/hypervisors/baremetal/src/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java b/plugins/hypervisors/baremetal/src/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java index 53683fd01b7..d2d0029701a 100644 --- a/plugins/hypervisors/baremetal/src/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java +++ b/plugins/hypervisors/baremetal/src/com/cloud/baremetal/manager/BareMetalTemplateAdapter.java @@ -84,20 +84,22 @@ public class BareMetalTemplateAdapter extends TemplateAdapterBase implements Tem @Override public VMTemplateVO create(TemplateProfile profile) { VMTemplateVO template = persistTemplate(profile, State.Active); - Long zoneId = profile.getZoneId(); + List zones = profile.getZoneIdList(); // create an entry at template_store_ref with store_id = null to represent that this template is ready for use. TemplateDataStoreVO vmTemplateHost = new TemplateDataStoreVO(null, template.getId(), new Date(), 100, Status.DOWNLOADED, null, null, null, null, template.getUrl()); this._tmpltStoreDao.persist(vmTemplateHost); - if (zoneId == null || zoneId == -1) { + if (zones == null) { List dcs = _dcDao.listAllIncludingRemoved(); if (dcs != null && dcs.size() > 0) { templateCreateUsage(template, dcs.get(0).getId()); } } else { - templateCreateUsage(template, zoneId); + for (Long zoneId: zones) { + templateCreateUsage(template, zoneId); + } } _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); @@ -123,8 +125,12 @@ public class BareMetalTemplateAdapter extends TemplateAdapterBase implements Tem boolean success = true; String zoneName; - if (!template.isCrossZones() && profile.getZoneId() != null) { - zoneName = profile.getZoneId().toString(); + if (profile.getZoneIdList() != null && profile.getZoneIdList().size() > 1) + throw new CloudRuntimeException("Operation is not supported for more than one zone id at a time"); + + if (!template.isCrossZones() && profile.getZoneIdList() != null) { + //get the first element in the list + zoneName = profile.getZoneIdList().get(0).toString(); } else { zoneName = "all zones"; } @@ -154,9 +160,16 @@ public class BareMetalTemplateAdapter extends TemplateAdapterBase implements Tem } } - if (profile.getZoneId() != null) { - UsageEventVO usageEvent = new UsageEventVO(eventType, account.getId(), profile.getZoneId(), templateId, null); + if (profile.getZoneIdList() != null) { + UsageEventVO usageEvent = new UsageEventVO(eventType, account.getId(), profile.getZoneIdList().get(0), + templateId, null); _usageEventDao.persist(usageEvent); + + VMTemplateZoneVO templateZone = _tmpltZoneDao.findByZoneTemplate(profile.getZoneIdList().get(0), templateId); + + if (templateZone != null) { + _tmpltZoneDao.remove(templateZone.getId()); + } } else { List dcs = _dcDao.listAllIncludingRemoved(); for (DataCenterVO dc : dcs) { @@ -165,12 +178,6 @@ public class BareMetalTemplateAdapter extends TemplateAdapterBase implements Tem } } - VMTemplateZoneVO templateZone = _tmpltZoneDao.findByZoneTemplate(profile.getZoneId(), templateId); - - if (templateZone != null) { - _tmpltZoneDao.remove(templateZone.getId()); - } - s_logger.debug("Successfully marked template host refs for template: " + template.getName() + " as destroyed in zone: " + zoneName); // If there are no more non-destroyed template host entries for this template, delete it diff --git a/server/src/com/cloud/api/ApiResponseHelper.java b/server/src/com/cloud/api/ApiResponseHelper.java index f0581863e4f..00262c8bdc6 100644 --- a/server/src/com/cloud/api/ApiResponseHelper.java +++ b/server/src/com/cloud/api/ApiResponseHelper.java @@ -1441,6 +1441,23 @@ public class ApiResponseHelper implements ResponseGenerator { return ViewResponseHelper.createTemplateResponse(view, tvo.toArray(new TemplateJoinVO[tvo.size()])); } + @Override + public List createTemplateResponses(ResponseView view, VirtualMachineTemplate result, + List zoneIds, boolean readyOnly) { + List tvo = null; + if (zoneIds == null) { + return createTemplateResponses(view, result, (Long)null, readyOnly); + } else { + for (Long zoneId: zoneIds){ + if (tvo == null) + tvo = ApiDBUtils.newTemplateView(result, zoneId, readyOnly); + else + tvo.addAll(ApiDBUtils.newTemplateView(result, zoneId, readyOnly)); + } + } + return ViewResponseHelper.createTemplateResponse(view, tvo.toArray(new TemplateJoinVO[tvo.size()])); + } + @Override public List createTemplateResponses(ResponseView view, long templateId, Long zoneId, boolean readyOnly) { VirtualMachineTemplate template = findTemplateById(templateId); diff --git a/server/src/com/cloud/storage/TemplateProfile.java b/server/src/com/cloud/storage/TemplateProfile.java index 81e34f3c121..1ca1d81fe8f 100644 --- a/server/src/com/cloud/storage/TemplateProfile.java +++ b/server/src/com/cloud/storage/TemplateProfile.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.storage; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import com.cloud.hypervisor.Hypervisor.HypervisorType; @@ -36,7 +38,7 @@ public class TemplateProfile { Boolean isExtractable; ImageFormat format; Long guestOsId; - Long zoneId; + List zoneIdList; HypervisorType hypervisorType; String accountName; Long domainId; @@ -51,8 +53,8 @@ public class TemplateProfile { TemplateType templateType; public TemplateProfile(Long templateId, Long userId, String name, String displayText, Integer bits, Boolean passwordEnabled, Boolean requiresHvm, String url, - Boolean isPublic, Boolean featured, Boolean isExtractable, ImageFormat format, Long guestOsId, Long zoneId, HypervisorType hypervisorType, - String accountName, Long domainId, Long accountId, String chksum, Boolean bootable, Map details, Boolean sshKeyEnabled) { + Boolean isPublic, Boolean featured, Boolean isExtractable, ImageFormat format, Long guestOsId, List zoneIdList, HypervisorType hypervisorType, + String accountName, Long domainId, Long accountId, String chksum, Boolean bootable, Map details, Boolean sshKeyEnabled) { this.templateId = templateId; this.userId = userId; this.name = name; @@ -66,7 +68,7 @@ public class TemplateProfile { this.isExtractable = isExtractable; this.format = format; this.guestOsId = guestOsId; - this.zoneId = zoneId; + this.zoneIdList = zoneIdList; this.hypervisorType = hypervisorType; this.accountName = accountName; this.domainId = domainId; @@ -80,11 +82,15 @@ public class TemplateProfile { public TemplateProfile(Long userId, VMTemplateVO template, Long zoneId) { this.userId = userId; this.template = template; - this.zoneId = zoneId; + if (zoneId != null) { + this.zoneIdList = new ArrayList<>(); + this.zoneIdList.add(zoneId); + } + else this.zoneIdList = null; } public TemplateProfile(Long templateId, Long userId, String name, String displayText, Integer bits, Boolean passwordEnabled, Boolean requiresHvm, String url, - Boolean isPublic, Boolean featured, Boolean isExtractable, ImageFormat format, Long guestOsId, Long zoneId, + Boolean isPublic, Boolean featured, Boolean isExtractable, ImageFormat format, Long guestOsId, List zoneId, HypervisorType hypervisorType, String accountName, Long domainId, Long accountId, String chksum, Boolean bootable, String templateTag, Map details, Boolean sshKeyEnabled, Long imageStoreId, Boolean isDynamicallyScalable, TemplateType templateType) { @@ -219,12 +225,8 @@ public class TemplateProfile { this.guestOsId = id; } - public Long getZoneId() { - return zoneId; - } - - public void setZoneId(Long id) { - this.zoneId = id; + public List getZoneIdList() { + return zoneIdList; } public HypervisorType getHypervisorType() { diff --git a/server/src/com/cloud/template/HypervisorTemplateAdapter.java b/server/src/com/cloud/template/HypervisorTemplateAdapter.java index bfb146bd5ee..49039d14b90 100644 --- a/server/src/com/cloud/template/HypervisorTemplateAdapter.java +++ b/server/src/com/cloud/template/HypervisorTemplateAdapter.java @@ -159,14 +159,31 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { throw new CloudRuntimeException("Unable to persist the template " + profile.getTemplate()); } + List zones = profile.getZoneIdList(); + + //zones is null when this template is to be registered to all zones + if (zones == null){ + createTemplateWithinZone(null, profile, template); + } + else { + for (Long zId : zones) { + createTemplateWithinZone(zId, profile, template); + } + } + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); + return template; + } + + private void createTemplateWithinZone(Long zId, TemplateProfile profile, VMTemplateVO template) { // find all eligible image stores for this zone scope - List imageStores = storeMgr.getImageStoresByScope(new ZoneScope(profile.getZoneId())); + List imageStores = storeMgr.getImageStoresByScope(new ZoneScope(zId)); if (imageStores == null || imageStores.size() == 0) { throw new CloudRuntimeException("Unable to find image store to download template " + profile.getTemplate()); } Set zoneSet = new HashSet(); - Collections.shuffle(imageStores); // For private templates choose a random store. TODO - Have a better algorithm based on size, no. of objects, load etc. + Collections.shuffle(imageStores); + // For private templates choose a random store. TODO - Have a better algorithm based on size, no. of objects, load etc. for (DataStore imageStore : imageStores) { // skip data stores for a disabled zone Long zoneId = imageStore.getScope().getScopeId(); @@ -179,23 +196,22 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { // Check if zone is disabled if (Grouping.AllocationState.Disabled == zone.getAllocationState()) { - s_logger.info("Zone " + zoneId + " is disabled, so skip downloading template to its image store " + imageStore.getId()); + s_logger.info("Zone " + zoneId + " is disabled. Skip downloading template to its image store " + imageStore.getId()); continue; } // Check if image store has enough capacity for template if (!_statsCollector.imageStoreHasEnoughCapacity(imageStore)) { - s_logger.info("Image store doesn't has enough capacity, so skip downloading template to this image store " + imageStore.getId()); + s_logger.info("Image store doesn't have enough capacity. Skip downloading template to this image store " + imageStore.getId()); continue; } // We want to download private template to one of the image store in a zone - if(isPrivateTemplate(template) && zoneSet.contains(zoneId)){ + if (isPrivateTemplate(template) && zoneSet.contains(zoneId)) { continue; - }else { + } else { zoneSet.add(zoneId); } } - TemplateInfo tmpl = imageFactory.getTemplate(template.getId(), imageStore); CreateTemplateContext context = new CreateTemplateContext(null, tmpl); AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); @@ -203,9 +219,6 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { caller.setContext(context); imageService.createTemplateAsync(tmpl, imageStore, caller); } - _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), ResourceType.template); - - return template; } @Override @@ -222,8 +235,15 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { throw new CloudRuntimeException("Unable to persist the template " + profile.getTemplate()); } + if (profile.getZoneIdList() != null && profile.getZoneIdList().size() > 1) + throw new CloudRuntimeException("Operation is not supported for more than one zone id at a time"); + + Long zoneId = null; + if (profile.getZoneIdList() != null) + zoneId = profile.getZoneIdList().get(0); + // find all eligible image stores for this zone scope - List imageStores = storeMgr.getImageStoresByScope(new ZoneScope(profile.getZoneId())); + List imageStores = storeMgr.getImageStoresByScope(new ZoneScope(zoneId)); if (imageStores == null || imageStores.size() == 0) { throw new CloudRuntimeException("Unable to find image store to download template " + profile.getTemplate()); } @@ -233,25 +253,27 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { Collections.shuffle(imageStores); // For private templates choose a random store. TODO - Have a better algorithm based on size, no. of objects, load etc. for (DataStore imageStore : imageStores) { // skip data stores for a disabled zone - Long zoneId = imageStore.getScope().getScopeId(); + Long zoneId_is = imageStore.getScope().getScopeId(); if (zoneId != null) { - DataCenterVO zone = _dcDao.findById(zoneId); + DataCenterVO zone = _dcDao.findById(zoneId_is); if (zone == null) { - s_logger.warn("Unable to find zone by id " + zoneId + ", so skip downloading template to its image store " + imageStore.getId()); + s_logger.warn("Unable to find zone by id " + zoneId_is + + ", so skip downloading template to its image store " + imageStore.getId()); continue; } // Check if zone is disabled if (Grouping.AllocationState.Disabled == zone.getAllocationState()) { - s_logger.info("Zone " + zoneId + " is disabled, so skip downloading template to its image store " + imageStore.getId()); + s_logger.info("Zone " + zoneId_is + + " is disabled, so skip downloading template to its image store " + imageStore.getId()); continue; } // We want to download private template to one of the image store in a zone - if (isPrivateTemplate(template) && zoneSet.contains(zoneId)) { + if (isPrivateTemplate(template) && zoneSet.contains(zoneId_is)) { continue; } else { - zoneSet.add(zoneId); + zoneSet.add(zoneId_is); } } @@ -363,8 +385,17 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { VMTemplateVO template = profile.getTemplate(); + if (profile.getZoneIdList() != null && profile.getZoneIdList().size() > 1) + throw new CloudRuntimeException("Operation is not supported for more than one zone id at a time"); + + Long zoneId = null; + if (profile.getZoneIdList() != null) + zoneId = profile.getZoneIdList().get(0); + // find all eligible image stores for this template - List imageStores = templateMgr.getImageStoreByTemplate(template.getId(), profile.getZoneId()); + List imageStores = templateMgr.getImageStoreByTemplate(template.getId(), + zoneId); + if (imageStores == null || imageStores.size() == 0) { // already destroyed on image stores s_logger.info("Unable to find image store still having template: " + template.getName() + ", so just mark the template removed"); @@ -427,7 +458,7 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { } } if (success) { - if ((imageStores.size() > 1) && (profile.getZoneId() != null)) { + if ((imageStores.size() > 1) && (profile.getZoneIdList() != null)) { //if template is stored in more than one image stores, and the zone id is not null, then don't delete other templates. return success; } @@ -467,13 +498,13 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { public TemplateProfile prepareDelete(DeleteTemplateCmd cmd) { TemplateProfile profile = super.prepareDelete(cmd); VMTemplateVO template = profile.getTemplate(); - Long zoneId = profile.getZoneId(); + List zoneIdList = profile.getZoneIdList(); if (template.getTemplateType() == TemplateType.SYSTEM) { throw new InvalidParameterValueException("The DomR template cannot be deleted."); } - if (zoneId != null && (storeMgr.getImageStore(zoneId) == null)) { + if (zoneIdList != null && (storeMgr.getImageStore(zoneIdList.get(0)) == null)) { throw new InvalidParameterValueException("Failed to find a secondary storage in the specified zone."); } @@ -483,9 +514,10 @@ public class HypervisorTemplateAdapter extends TemplateAdapterBase { @Override public TemplateProfile prepareDelete(DeleteIsoCmd cmd) { TemplateProfile profile = super.prepareDelete(cmd); - Long zoneId = profile.getZoneId(); + List zoneIdList = profile.getZoneIdList(); - if (zoneId != null && (storeMgr.getImageStore(zoneId) == null)) { + if (zoneIdList != null && + (storeMgr.getImageStore(zoneIdList.get(0)) == null)) { throw new InvalidParameterValueException("Failed to find a secondary storage in the specified zone."); } diff --git a/server/src/com/cloud/template/TemplateAdapter.java b/server/src/com/cloud/template/TemplateAdapter.java index 4ed0a81ec6a..7233ac98751 100644 --- a/server/src/com/cloud/template/TemplateAdapter.java +++ b/server/src/com/cloud/template/TemplateAdapter.java @@ -70,11 +70,11 @@ public interface TemplateAdapter extends Adapter { public boolean delete(TemplateProfile profile); public TemplateProfile prepare(boolean isIso, Long userId, String name, String displayText, Integer bits, Boolean passwordEnabled, Boolean requiresHVM, String url, - Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, Long zoneId, HypervisorType hypervisorType, String accountName, + Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, List zoneId, HypervisorType hypervisorType, String accountName, Long domainId, String chksum, Boolean bootable, Map details) throws ResourceAllocationException; public TemplateProfile prepare(boolean isIso, long userId, String name, String displayText, Integer bits, Boolean passwordEnabled, Boolean requiresHVM, String url, - Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, Long zoneId, HypervisorType hypervisorType, String chksum, + Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, List zoneId, HypervisorType hypervisorType, String chksum, Boolean bootable, String templateTag, Account templateOwner, Map details, Boolean sshKeyEnabled, String imageStoreUuid, Boolean isDynamicallyScalable, TemplateType templateType) throws ResourceAllocationException; diff --git a/server/src/com/cloud/template/TemplateAdapterBase.java b/server/src/com/cloud/template/TemplateAdapterBase.java index 530c562b28d..1e2c6d2178a 100644 --- a/server/src/com/cloud/template/TemplateAdapterBase.java +++ b/server/src/com/cloud/template/TemplateAdapterBase.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.template; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -119,7 +120,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat @Override public TemplateProfile prepare(boolean isIso, Long userId, String name, String displayText, Integer bits, Boolean passwordEnabled, Boolean requiresHVM, String url, - Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, Long zoneId, HypervisorType hypervisorType, String accountName, + Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, List zoneId, HypervisorType hypervisorType, String accountName, Long domainId, String chksum, Boolean bootable, Map details) throws ResourceAllocationException { return prepare(isIso, userId, name, displayText, bits, passwordEnabled, requiresHVM, url, isPublic, featured, isExtractable, format, guestOSId, zoneId, hypervisorType, chksum, bootable, null, null, details, false, null, false, TemplateType.USER); @@ -127,7 +128,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat @Override public TemplateProfile prepare(boolean isIso, long userId, String name, String displayText, Integer bits, Boolean passwordEnabled, Boolean requiresHVM, String url, - Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, Long zoneId, HypervisorType hypervisorType, String chksum, + Boolean isPublic, Boolean featured, Boolean isExtractable, String format, Long guestOSId, List zoneIdList, HypervisorType hypervisorType, String chksum, Boolean bootable, String templateTag, Account templateOwner, Map details, Boolean sshkeyEnabled, String imageStoreUuid, Boolean isDynamicallyScalable, TemplateType templateType) throws ResourceAllocationException { //Long accountId = null; @@ -137,10 +138,6 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat isPublic = Boolean.FALSE; } - if (zoneId.longValue() == -1) { - zoneId = null; - } - if (isIso) { if (bootable == null) { bootable = Boolean.TRUE; @@ -178,7 +175,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat isRegionStore = true; } - if (!isAdmin && zoneId == null && !isRegionStore ) { + if (!isAdmin && zoneIdList == null && !isRegionStore ) { // domain admin and user should also be able to register template on a region store throw new InvalidParameterValueException("Please specify a valid zone Id. Only admins can create templates in all zones."); } @@ -215,14 +212,16 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat _resourceLimitMgr.checkResourceLimit(templateOwner, ResourceType.template); // If a zoneId is specified, make sure it is valid - if (zoneId != null) { - DataCenterVO zone = _dcDao.findById(zoneId); - if (zone == null) { - throw new IllegalArgumentException("Please specify a valid zone."); - } - Account caller = CallContext.current().getCallingAccount(); - if(Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())){ - throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: "+ zoneId ); + if (zoneIdList != null) { + for (Long zoneId :zoneIdList) { + DataCenterVO zone = _dcDao.findById(zoneId); + if (zone == null) { + throw new IllegalArgumentException("Please specify a valid zone."); + } + Account caller = CallContext.current().getCallingAccount(); + if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())) { + throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: " + zoneId); + } } } @@ -248,7 +247,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat Long id = _tmpltDao.getNextInSequence(Long.class, "id"); CallContext.current().setEventDetails("Id: " + id + " name: " + name); - return new TemplateProfile(id, userId, name, displayText, bits, passwordEnabled, requiresHVM, url, isPublic, featured, isExtractable, imgfmt, guestOSId, zoneId, + return new TemplateProfile(id, userId, name, displayText, bits, passwordEnabled, requiresHVM, url, isPublic, featured, isExtractable, imgfmt, guestOSId, zoneIdList, hypervisorType, templateOwner.getAccountName(), templateOwner.getDomainId(), templateOwner.getAccountId(), chksum, bootable, templateTag, details, sshkeyEnabled, null, isDynamicallyScalable, templateType); @@ -263,11 +262,11 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat boolean isRouting = (cmd.isRoutingType() == null) ? false : cmd.isRoutingType(); - Long zoneId = cmd.getZoneId(); + List zoneId = cmd.getZoneIds(); // ignore passed zoneId if we are using region wide image store List stores = _imgStoreDao.findRegionImageStores(); if (stores != null && stores.size() > 0) { - zoneId = -1L; + zoneId = null; } HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); @@ -291,11 +290,13 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat boolean isRouting = (cmd.isRoutingType() == null) ? false : cmd.isRoutingType(); + List zoneList = null; Long zoneId = cmd.getZoneId(); // ignore passed zoneId if we are using region wide image store List stores = _imgStoreDao.findRegionImageStores(); - if (stores != null && stores.size() > 0) { - zoneId = -1L; + if (!(stores != null && stores.size() > 0)) { + zoneList = new ArrayList<>(); + zoneList.add(zoneId); } HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); @@ -305,7 +306,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat } return prepare(false, CallContext.current().getCallingUserId(), cmd.getName(), cmd.getDisplayText(), cmd.getBits(), cmd.isPasswordEnabled(), - cmd.getRequiresHvm(), null, cmd.isPublic(), cmd.isFeatured(), cmd.isExtractable(), cmd.getFormat(), cmd.getOsTypeId(), zoneId, + cmd.getRequiresHvm(), null, cmd.isPublic(), cmd.isFeatured(), cmd.isExtractable(), cmd.getFormat(), cmd.getOsTypeId(), zoneList, hypervisorType, cmd.getChecksum(), true, cmd.getTemplateTag(), owner, cmd.getDetails(), cmd.isSshKeyEnabled(), null, cmd.isDynamicallyScalable(), isRouting ? TemplateType.ROUTING : TemplateType.USER); @@ -318,20 +319,22 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId()); _accountMgr.checkAccess(caller, null, true, owner); + List zoneList = null; Long zoneId = cmd.getZoneId(); // ignore passed zoneId if we are using region wide image store List stores = _imgStoreDao.findRegionImageStores(); - if (stores != null && stores.size() > 0) { - zoneId = -1L; + if (!(stores != null && stores.size() > 0)) { + zoneList = new ArrayList<>(); + zoneList.add(zoneId); } return prepare(true, CallContext.current().getCallingUserId(), cmd.getIsoName(), cmd.getDisplayText(), 64, false, true, cmd.getUrl(), cmd.isPublic(), - cmd.isFeatured(), cmd.isExtractable(), ImageFormat.ISO.toString(), cmd.getOsTypeId(), zoneId, HypervisorType.None, cmd.getChecksum(), cmd.isBootable(), null, + cmd.isFeatured(), cmd.isExtractable(), ImageFormat.ISO.toString(), cmd.getOsTypeId(), zoneList, HypervisorType.None, cmd.getChecksum(), cmd.isBootable(), null, owner, null, false, cmd.getImageStoreUuid(), cmd.isDynamicallyScalable(), TemplateType.USER); } protected VMTemplateVO persistTemplate(TemplateProfile profile, VirtualMachineTemplate.State initialState) { - Long zoneId = profile.getZoneId(); + List zoneIdList = profile.getZoneIdList(); VMTemplateVO template = new VMTemplateVO(profile.getTemplateId(), profile.getName(), profile.getFormat(), profile.getIsPublic(), profile.getFeatured(), profile.getIsExtractable(), profile.getTemplateType(), profile.getUrl(), profile.getRequiresHVM(), profile.getBits(), profile.getAccountId(), profile.getCheckSum(), @@ -339,7 +342,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat profile.getTemplateTag(), profile.getDetails(), profile.getSshKeyEnabled(), profile.IsDynamicallyScalable()); template.setState(initialState); - if (zoneId == null || zoneId.longValue() == -1) { + if (zoneIdList == null) { List dcs = _dcDao.listAll(); if (dcs.isEmpty()) { @@ -352,7 +355,9 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat } } else { - _tmpltDao.addTemplateToZone(template, zoneId); + for (Long zoneId: zoneIdList) { + _tmpltDao.addTemplateToZone(template, zoneId); + } } return _tmpltDao.findById(template.getId()); } diff --git a/server/src/com/cloud/template/TemplateManagerImpl.java b/server/src/com/cloud/template/TemplateManagerImpl.java index fd6b623c06f..1b7eb53e59c 100644 --- a/server/src/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/com/cloud/template/TemplateManagerImpl.java @@ -33,7 +33,11 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import com.cloud.storage.ImageStoreUploadMonitorImpl; +import com.cloud.utils.StringUtils; import com.cloud.utils.EncryptionUtil; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.EnumUtils; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -177,9 +181,6 @@ import com.cloud.user.AccountVO; import com.cloud.user.ResourceLimitService; import com.cloud.user.dao.AccountDao; import com.cloud.uservm.UserVm; -import com.cloud.utils.DateUtil; -import com.cloud.utils.EnumUtils; -import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; @@ -780,7 +781,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, Long templateId = cmd.getId(); Long userId = CallContext.current().getCallingUserId(); Long sourceZoneId = cmd.getSourceZoneId(); - Long destZoneId = cmd.getDestinationZoneId(); + List destZoneIds = cmd.getDestinationZoneIds(); Account caller = CallContext.current().getCallingAccount(); // Verify parameters @@ -789,28 +790,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, throw new InvalidParameterValueException("Unable to find template with id"); } - DataStore srcSecStore = null; if (sourceZoneId != null) { - // template is on zone-wide secondary storage - srcSecStore = getImageStore(sourceZoneId, templateId); - } else { - // template is on region store - srcSecStore = getImageStore(templateId); - } - - if (srcSecStore == null) { - throw new InvalidParameterValueException("There is no template " + templateId + " ready on image store."); - } - - if (template.isCrossZones()) { - // sync template from cache store to region store if it is not there, for cases where we are going to migrate existing NFS to S3. - _tmpltSvr.syncTemplateToRegionStore(templateId, srcSecStore); - s_logger.debug("Template " + templateId + " is cross-zone, don't need to copy"); - return template; - } - - if (sourceZoneId != null) { - if (sourceZoneId.equals(destZoneId)) { + if (destZoneIds!= null && destZoneIds.contains(sourceZoneId)) { throw new InvalidParameterValueException("Please specify different source and destination zones."); } @@ -820,26 +801,77 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, } } - DataCenterVO dstZone = _dcDao.findById(destZoneId); - if (dstZone == null) { - throw new InvalidParameterValueException("Please specify a valid destination zone."); - } + Map dataCenterVOs = new HashMap(); - DataStore dstSecStore = getImageStore(destZoneId, templateId); - if (dstSecStore != null) { - s_logger.debug("There is template " + templateId + " in secondary storage " + dstSecStore.getName() + " in zone " + destZoneId + " , don't need to copy"); - return template; + for (Long destZoneId: destZoneIds) { + DataCenterVO dstZone = _dcDao.findById(destZoneId); + if (dstZone == null) { + throw new InvalidParameterValueException("Please specify a valid destination zone."); + } + dataCenterVOs.put(destZoneId, dstZone); } _accountMgr.checkAccess(caller, AccessType.OperateEntry, true, template); - boolean success = copy(userId, template, srcSecStore, dstZone); + List failedZones = new ArrayList<>(); - if (success) { - // increase resource count - long accountId = template.getAccountId(); - if (template.getSize() != null) { - _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.secondary_storage, template.getSize()); + boolean success = false; + if (template.getHypervisorType() == HypervisorType.BareMetal) { + if (template.isCrossZones()) { + s_logger.debug("Template " + templateId + " is cross-zone, don't need to copy"); + return template; + } + for (Long destZoneId: destZoneIds) { + if (!addTemplateToZone(template, destZoneId, sourceZoneId)) { + failedZones.add(dataCenterVOs.get(destZoneId).getName()); + } + } + } else { + DataStore srcSecStore = null; + if (sourceZoneId != null) { + // template is on zone-wide secondary storage + srcSecStore = getImageStore(sourceZoneId, templateId); + } else { + // template is on region store + srcSecStore = getImageStore(templateId); + } + + if (srcSecStore == null) { + throw new InvalidParameterValueException("There is no template " + templateId + " ready on image store."); + } + + if (template.isCrossZones()) { + // sync template from cache store to region store if it is not there, for cases where we are going to migrate existing NFS to S3. + _tmpltSvr.syncTemplateToRegionStore(templateId, srcSecStore); + s_logger.debug("Template " + templateId + " is cross-zone, don't need to copy"); + return template; + } + for (Long destZoneId : destZoneIds) { + DataStore dstSecStore = getImageStore(destZoneId, templateId); + if (dstSecStore != null) { + s_logger.debug("There is template " + templateId + " in secondary storage " + dstSecStore.getName() + + " in zone " + destZoneId + " , don't need to copy"); + continue; + } + if (!copy(userId, template, srcSecStore, dataCenterVOs.get(destZoneId))) { + failedZones.add(dataCenterVOs.get(destZoneId).getName()); + } + else{ + if (template.getSize() != null) { + // increase resource count + long accountId = template.getAccountId(); + _resourceLimitMgr.incrementResourceCount(accountId, ResourceType.secondary_storage, template.getSize()); + } + } + } + + + } + + if ((destZoneIds != null) && (destZoneIds.size() > failedZones.size())){ + if (!failedZones.isEmpty()) { + s_logger.debug("There were failures when copying template to zones: " + + StringUtils.listToCsvTags(failedZones)); } return template; } else { @@ -847,6 +879,25 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, } } + private boolean addTemplateToZone(VMTemplateVO template, long dstZoneId, long sourceZoneid) throws ResourceAllocationException{ + long tmpltId = template.getId(); + DataCenterVO dstZone = _dcDao.findById(dstZoneId); + DataCenterVO sourceZone = _dcDao.findById(sourceZoneid); + + AccountVO account = _accountDao.findById(template.getAccountId()); + + + _resourceLimitMgr.checkResourceLimit(account, ResourceType.template); + + try { + _tmpltDao.addTemplateToZone(template, dstZoneId); + return true; + } catch (Exception ex) { + s_logger.debug("failed to copy template from Zone: " + sourceZone.getUuid() + " to Zone: " + dstZone.getUuid()); + } + return false; + } + @Override public boolean delete(long userId, long templateId, Long zoneId) { VMTemplateVO template = _tmpltDao.findById(templateId); diff --git a/server/test/com/cloud/template/HypervisorTemplateAdapterTest.java b/server/test/com/cloud/template/HypervisorTemplateAdapterTest.java index 50ed8707326..3a6774821f4 100644 --- a/server/test/com/cloud/template/HypervisorTemplateAdapterTest.java +++ b/server/test/com/cloud/template/HypervisorTemplateAdapterTest.java @@ -241,7 +241,7 @@ public class HypervisorTemplateAdapterTest { TemplateProfile profile = mock(TemplateProfile.class); when(profile.getTemplate()).thenReturn(template); - when(profile.getZoneId()).thenReturn(1l); + when(profile.getZoneIdList()).thenReturn(null); TemplateApiResult result = mock(TemplateApiResult.class); when(result.isSuccess()).thenReturn(true); diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 40d503c04f2..636f7532d64 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -13270,4 +13270,32 @@ ul.ui-autocomplete.ui-menu { .list-view-select table th.availableHostSuitability, .list-view-select table td.availableHostSuitability { max-width: 250px; -} \ No newline at end of file +} +.copy-template-destination-list div.text-search { + right: 5px; +} + +.copy-template-destination-list div.ui-widget-content { + display: block !important; +} + +div.panel.copy-template-destination-list div.list-view div.fixed-header{ + width: 500px; +} + +.copy-template-destination-list.panel div.list-view div.data-table table{ + width: 595px; +} + +.copy-template-destination-list .list-view .toolbar { + width: 654px; +} + +.multi-edit-add-list .ui-button.copytemplateok{ + left: 330px; +} + +.multi-edit-add-list .ui-button.copytemplatecancel { + left: 310px; +} + diff --git a/ui/index.html b/ui/index.html index 407e3a75a53..41894a65f83 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1853,6 +1853,7 @@ + diff --git a/ui/l10n/en.js b/ui/l10n/en.js index bc79383dc19..b30e7b1b3b5 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -2207,6 +2207,7 @@ var dictionary = {"ICMP.code":"ICMP Code", "message.suspend.project":"Are you sure you want to suspend this project?", "message.systems.vms.ready":"System VMs ready.", "message.template.copying":"Template is being copied.", +"message.template.copy.select.zone":"Please select a zone to copy template.", "message.template.desc":"OS image that can be used to boot VMs", "message.tier.required":"Tier is required", "message.tooltip.dns.1":"Name of a DNS server for use by VMs in the zone. The public IP addresses for the zone must have a route to this server.", diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js index 11622f1f7de..938514f84d2 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -1197,7 +1197,7 @@ cloudStack.docs = { externalLink: '' }, helpRegisterTemplateZone: { - desc: 'Choose the zone where you want the template to be available, or All Zones to make it available throughout the cloud', + desc: 'Choose one or more zones where you want the template to be available, or All Zones to make it available throughout the cloud. (Tip: Use Ctrl to choose multiple zones)', externalLink: '' }, helpRegisterTemplateHypervisor: { @@ -1333,4 +1333,4 @@ cloudStack.docs = { helpLdapLinkDomainAdmin: { desc: 'domain admin of the linked domain. Specify a username in GROUP/OU of LDAP' } -}; \ No newline at end of file +}; diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index f845fd84c67..73d6f87e066 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -2354,3 +2354,13 @@ $.validator.addMethod("ipv46cidr", function(value, element) { return false; }, "The specified IPv4/IPv6 CIDR is invalid."); + + +$.validator.addMethod("allzonesonly", function(value, element){ + + if ((value.indexOf("-1") != -1) &&(value.length > 1)) + return false; + return true; + +}, +"All Zones cannot be combined with any other zone"); diff --git a/ui/scripts/templates.js b/ui/scripts/templates.js index f19d55c943d..e79c13c9ecd 100755 --- a/ui/scripts/templates.js +++ b/ui/scripts/templates.js @@ -137,6 +137,10 @@ zone: { label: 'label.zone', docID: 'helpRegisterTemplateZone', + isMultiple: true, + validation: { + allzonesonly: true + }, select: function(args) { if(g_regionsecondaryenabled == true) { args.response.success({ @@ -182,35 +186,50 @@ select: function(args) { if (args.zone == null) return; + // We want only distinct Hypervisor entries to be visible to the user + var items = []; + var distinctHVNames = []; + var length = 1; + // When only one zone is selected, args.zone is NOT an array. + if (Object.prototype.toString.call( args.zone ) === '[object Array]') + length = args.zone.length; + for (var index = 0; index < length; index++) + { + var zoneId; + if (length == 1) + zoneId = args.zone; + else + zoneId = args.zone[index]; - var apiCmd; - if (args.zone == -1) { //All Zones - //apiCmd = "listHypervisors&zoneid=-1"; //"listHypervisors&zoneid=-1" has been changed to return only hypervisors available in all zones (bug 8809) - apiCmd = "listHypervisors"; - } - else { - apiCmd = "listHypervisors&zoneid=" + args.zone; - } - - $.ajax({ - url: createURL(apiCmd), - dataType: "json", - async: false, - success: function(json) { - var hypervisorObjs = json.listhypervisorsresponse.hypervisor; - var items = []; - $(hypervisorObjs).each(function() { - items.push({ - id: this.name, - description: this.name - }); - }); - args.response.success({ - data: items - }); + var apiCmd; + if (zoneId == -1) { //All Zones + apiCmd = "listHypervisors"; + } + else { + apiCmd = "listHypervisors&zoneid=" + zoneId; } - }); + $.ajax({ + url: createURL(apiCmd), + dataType: "json", + async: false, + success: function(json) { + var hypervisorObjs = json.listhypervisorsresponse.hypervisor; + + $(hypervisorObjs).each(function() { + // Only if this hypervisor isn't already part of this + // list, then add to the drop down + if (distinctHVNames.indexOf(this.name) < 0 ){ + distinctHVNames.push(this.name); + items.push({ + id: this.name, + description: this.name + }); + } + }); + } + }); + } args.$select.change(function() { var $form = $(this).closest('form'); if ($(this).val() == "VMware") { @@ -244,7 +263,15 @@ } }); + items.push({ + id: "Any", + description: "Any" + }); + args.response.success({ + data: items + }); args.$select.trigger('change'); + } }, @@ -545,11 +572,17 @@ }, action: function(args) { + var zones = ""; + if (Object.prototype.toString.call( args.data.zone ) === '[object Array]'){ + zones = args.data.zone.join(","); + } + else + zones = args.data.zone; var data = { name: args.data.name, displayText: args.data.description, url: args.data.url, - zoneid: args.data.zone, + zoneids: zones, format: args.data.format, isextractable: (args.data.isExtractable == "on"), passwordEnabled: (args.data.isPasswordEnabled == "on"), @@ -1532,9 +1565,6 @@ copyTemplate: { label: 'label.action.copy.template', messages: { - confirm: function(args) { - return 'message.copy.template.confirm'; - }, success: function(args) { return 'message.template.copying'; }, @@ -1542,76 +1572,102 @@ return 'label.action.copy.template'; } }, - createForm: { - title: 'label.action.copy.template', - desc: '', - fields: { - destinationZoneId: { - label: 'label.destination.zone', - docID: 'helpCopyTemplateDestination', - validation: { - required: true - }, - select: function(args) { - $.ajax({ - url: createURL("listZones&available=true"), - dataType: "json", - async: true, - success: function(json) { - var zoneObjs = []; - var items = json.listzonesresponse.zone; - if (items != null) { - for (var i = 0; i < items.length; i++) { - if (args.context.zones[0].zoneid != items[i].id) { - zoneObjs.push({ - id: items[i].id, - description: items[i].name + action: { + custom: cloudStack.uiCustom.copyTemplate({ + listView: { + listView: { + id: 'destinationZones', + fields: { + destinationZoneName: { + label: 'label.name' + } + }, + dataProvider: function(args) { + var data = { + page: args.page, + pagesize: pageSize + }; + if (args.filterBy.search.value) { + data.keyword = args.filterBy.search.value; + } + $.ajax({ + url: createURL("listZones&available=true"), + dataType: "json", + data: data, + async: true, + success: function(json) { + var zoneObjs = []; + var items = json.listzonesresponse.zone; + if (items != null) { + for (var i = 0; i < items.length; i++) { + if (args.context.zones[0].zoneid != items[i].id) { + zoneObjs.push({ + id: items[i].id, + destinationZoneName: items[i].name + }); + } + } + args.response.success({ + data: zoneObjs }); - } + }else if(args.page == 1) { + args.response.success({ + data: [] + }); + } else { + args.response.success({ + data: [] + }); + } + } + }); + } + } + }, + action: function(args) { + var zoneids = ""; + if (args.context.selectedZone != null && + args.context.selectedZone.length > 0) { + for (var i = 0; i < args.context.selectedZone.length; i++){ + if (i != 0 ) + zoneids += ","; + zoneids += args.context.selectedZone[i].id; + } + } + if (zoneids == "") + return; + var data = { + id: args.context.templates[0].id, + destzoneids: zoneids, + sourcezoneid: args.context.zones[0].zoneid + }; + + $.ajax({ + url: createURL('copyTemplate'), + data: data, + success: function(json) { + var jid = json.copytemplateresponse.jobid; + args.response.success({ + _custom: { + jobId: jid, + getUpdatedItem: function(json) { + return {}; //nothing in this template needs to be updated + }, + getActionFilter: function() { + return templateActionfilter; } } - args.response.success({ - data: zoneObjs - }); - } - }); - } - } - } - }, - action: function(args) { - var data = { - id: args.context.templates[0].id, - destzoneid: args.data.destinationZoneId - }; - $.extend(data, { - sourcezoneid: args.context.zones[0].zoneid - }); - - $.ajax({ - url: createURL('copyTemplate'), - data: data, - success: function(json) { - var jid = json.copytemplateresponse.jobid; - args.response.success({ - _custom: { - jobId: jid, - getUpdatedItem: function(json) { - return {}; //nothing in this template needs to be updated - }, - getActionFilter: function() { - return templateActionfilter; - } + }); } }); } - }); + }) }, notification: { poll: pollAsyncJobResult } - } - }, + } + }, tabs: { details: { diff --git a/ui/scripts/ui-custom/copyTemplate.js b/ui/scripts/ui-custom/copyTemplate.js new file mode 100644 index 00000000000..60125fe1409 --- /dev/null +++ b/ui/scripts/ui-custom/copyTemplate.js @@ -0,0 +1,130 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +(function(cloudStack, $) { + cloudStack.uiCustom.copyTemplate = function(args) { + var listView = args.listView; + var action = args.action; + + return function(args) { + var context = args.context; + + var destZoneList = function(args) { + var $listView; + + var destZones = $.extend(true, {}, args.listView, { + context: context, + uiCustom: true + }); + + destZones.listView.actions = { + select: { + label: _l('label.select.zone'), + type: 'checkbox', + action: { + uiCustom: function(args) { + var $item = args.$item; + var $input = $item.find('td.actions input:visible'); + + if ($input.attr('type') == 'checkbox') { + if ($input.is(':checked')) + $item.addClass('multi-edit-selected'); + else + $item.removeClass('multi-edit-selected'); + } else { + $item.siblings().removeClass('multi-edit-selected'); + $item.addClass('multi-edit-selected'); + } + } + } + } + }; + + $listView = $('
').listView(destZones); + + // Change action label + $listView.find('th.actions').html(_l('label.select')); + + return $listView; + }; + + var $dataList = destZoneList({ + listView: listView + }).dialog({ + dialogClass: 'multi-edit-add-list panel copy-template-destination-list', + width: 625, + draggable: false, + title: _l('label.action.copy.template'), + buttons: [{ + text: _l('label.ok'), + 'class': 'ok copytemplateok', + click: function() { + var complete = args.complete; + var selectedZoneObj = []; + $dataList.find('tr.multi-edit-selected').map(function(index, elem) { + var itemData = $(elem).data('json-obj'); + selectedZoneObj.push(itemData) ; + }); + if(selectedZoneObj != undefined) { + $dataList.fadeOut(function() { + action({ + context: $.extend(true, {}, context, { + selectedZone: selectedZoneObj + }), + response: { + success: function(args) { + complete({ + _custom: args._custom, + $item: $('
'), + }); + }, + error: function(args) { + cloudStack.dialog.notice({ + message: args + }); + } + } + }); + }); + + $('div.overlay').fadeOut(function() { + $('div.overlay').remove(); + }); + } + else { + cloudStack.dialog.notice({ + message: _l('message.template.copy.select.zone') + }); + } + + } + }, { + text: _l('label.cancel'), + 'class': 'cancel copytemplatecancel', + click: function() { + $dataList.fadeOut(function() { + $dataList.remove(); + }); + $('div.overlay').fadeOut(function() { + $('div.overlay').remove(); + $(':ui-dialog').dialog('destroy'); + }); + } + }] + }).parent('.ui-dialog').overlay(); + }; + }; +}(cloudStack, jQuery)); diff --git a/ui/scripts/ui/dialog.js b/ui/scripts/ui/dialog.js index 2f3f84712d5..6f4fedcac0f 100644 --- a/ui/scripts/ui/dialog.js +++ b/ui/scripts/ui/dialog.js @@ -276,6 +276,11 @@ // Determine field type of input if (field.select) { + var multiple = false; + if (field.isMultiple != null){ + if (typeof(field.isMultiple) == 'boolean' && field.isMultiple == true) + multiple = true; + } isAsync = true; selectArgs = { context: args.context, @@ -321,10 +326,18 @@ }; selectFn = field.select; - $input = $('') + .attr(attrib) .data('dialog-select-fn', function(args) { selectFn(args ? $.extend(true, {}, selectArgs, args) : selectArgs); })