diff --git a/.travis.yml b/.travis.yml index e5ad914562f..18564f9304e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,7 @@ env: smoke/test_affinity_groups smoke/test_affinity_groups_projects smoke/test_async_job + smoke/test_backup_recovery_dummy smoke/test_create_list_domain_account_project smoke/test_create_network smoke/test_deploy_vgpu_enabled_vm diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 33950f8f278..c74e9b7b353 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -478,6 +478,17 @@ public class EventTypes { public static final String EVENT_VM_SNAPSHOT_OFF_PRIMARY = "VMSNAPSHOT.OFF_PRIMARY"; public static final String EVENT_VM_SNAPSHOT_REVERT = "VMSNAPSHOT.REVERTTO"; + // Backup and Recovery events + public static final String EVENT_VM_BACKUP_IMPORT_OFFERING = "BACKUP.IMPORT.OFFERING"; + public static final String EVENT_VM_BACKUP_OFFERING_ASSIGN = "BACKUP.OFFERING.ASSIGN"; + public static final String EVENT_VM_BACKUP_OFFERING_REMOVE = "BACKUP.OFFERING.REMOVE"; + public static final String EVENT_VM_BACKUP_CREATE = "BACKUP.CREATE"; + public static final String EVENT_VM_BACKUP_RESTORE = "BACKUP.RESTORE"; + public static final String EVENT_VM_BACKUP_DELETE = "BACKUP.DELETE"; + public static final String EVENT_VM_BACKUP_RESTORE_VOLUME_TO_VM = "BACKUP.RESTORE.VOLUME.TO.VM"; + public static final String EVENT_VM_BACKUP_SCHEDULE_CONFIGURE = "BACKUP.SCHEDULE.CONFIGURE"; + public static final String EVENT_VM_BACKUP_SCHEDULE_DELETE = "BACKUP.SCHEDULE.DELETE"; + // external network device events public static final String EVENT_EXTERNAL_NVP_CONTROLLER_ADD = "PHYSICAL.NVPCONTROLLER.ADD"; public static final String EVENT_EXTERNAL_NVP_CONTROLLER_DELETE = "PHYSICAL.NVPCONTROLLER.DELETE"; diff --git a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java index da2c7d04eb3..8a109649e96 100644 --- a/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java +++ b/api/src/main/java/com/cloud/hypervisor/HypervisorGuru.java @@ -19,13 +19,14 @@ package com.cloud.hypervisor; import java.util.List; import java.util.Map; -import com.cloud.storage.StoragePool; +import org.apache.cloudstack.backup.Backup; import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.agent.api.Command; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.storage.StoragePool; import com.cloud.utils.Pair; import com.cloud.utils.component.Adapter; import com.cloud.vm.NicProfile; @@ -86,6 +87,11 @@ public interface HypervisorGuru extends Adapter { Map getClusterSettings(long vmId); + VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, + String vmInternalName, Backup backup) throws Exception; + + boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, + VirtualMachine vm, long poolId, Backup backup) throws Exception; /** * Will generate commands to migrate a vm to a pool. For now this will only work for stopped VMs on Vmware. * diff --git a/api/src/main/java/com/cloud/server/ResourceTag.java b/api/src/main/java/com/cloud/server/ResourceTag.java index 99eb8603d1c..fb07762639a 100644 --- a/api/src/main/java/com/cloud/server/ResourceTag.java +++ b/api/src/main/java/com/cloud/server/ResourceTag.java @@ -29,6 +29,7 @@ public interface ResourceTag extends ControlledEntity, Identity, InternalIdentit ISO(true, false), Volume(true, true), Snapshot(true, false), + Backup(true, false), Network(true, true), Nic(false, true), LoadBalancer(true, true), diff --git a/api/src/main/java/com/cloud/vm/VirtualMachine.java b/api/src/main/java/com/cloud/vm/VirtualMachine.java index d234c488fd0..4d6014f0a94 100644 --- a/api/src/main/java/com/cloud/vm/VirtualMachine.java +++ b/api/src/main/java/com/cloud/vm/VirtualMachine.java @@ -16,18 +16,21 @@ // under the License. package com.cloud.vm; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Displayable; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.kernel.Partition; + import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.utils.fsm.StateMachine2; import com.cloud.utils.fsm.StateMachine2.Transition; import com.cloud.utils.fsm.StateMachine2.Transition.Impact; import com.cloud.utils.fsm.StateObject; -import org.apache.cloudstack.acl.ControlledEntity; -import org.apache.cloudstack.api.Displayable; -import org.apache.cloudstack.kernel.Partition; - -import java.util.Arrays; -import java.util.Date; -import java.util.Map; /** * VirtualMachine describes the properties held by a virtual machine @@ -319,6 +322,12 @@ public interface VirtualMachine extends RunningOn, ControlledEntity, Partition, Long getDiskOfferingId(); + Long getBackupOfferingId(); + + String getBackupExternalId(); + + List getBackupVolumeList(); + Type getType(); HypervisorType getHypervisorType(); diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandJobType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandJobType.java index d35598b508d..1cac1daba19 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandJobType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandJobType.java @@ -23,6 +23,7 @@ public enum ApiCommandJobType { Volume, ConsoleProxy, Snapshot, + Backup, Template, Iso, SystemVm, diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 015061cd2d3..1c6fd8b9e94 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -33,6 +33,9 @@ public class ApiConstants { public static final String APPLIED = "applied"; public static final String LIST_LB_VMIPS = "lbvmips"; public static final String AVAILABLE = "available"; + public static final String BACKUP_ID = "backupid"; + public static final String BACKUP_OFFERING_NAME = "backupofferingname"; + public static final String BACKUP_OFFERING_ID = "backupofferingid"; public static final String BITS = "bits"; public static final String BOOTABLE = "bootable"; public static final String BIND_DN = "binddn"; @@ -144,6 +147,7 @@ public class ApiConstants { public static final String EXTRA_DHCP_OPTION_NAME = "extradhcpoptionname"; public static final String EXTRA_DHCP_OPTION_CODE = "extradhcpoptioncode"; public static final String EXTRA_DHCP_OPTION_VALUE = "extradhcpvalue"; + public static final String EXTERNAL = "external"; public static final String FENCE = "fence"; public static final String FETCH_LATEST = "fetchlatest"; public static final String FIRSTNAME = "firstname"; @@ -366,6 +370,7 @@ public class ApiConstants { public static final String VALUE = "value"; public static final String VIRTUAL_MACHINE_ID = "virtualmachineid"; public static final String VIRTUAL_MACHINE_IDS = "virtualmachineids"; + public static final String VIRTUAL_MACHINE_NAME = "virtualmachinename"; public static final String VIRTUAL_MACHINE_ID_IP = "vmidipmap"; public static final String VIRTUAL_MACHINE_COUNT = "virtualmachinecount"; public static final String USAGE_ID = "usageid"; @@ -385,6 +390,7 @@ public class ApiConstants { public static final String VNET = "vnet"; public static final String IS_VOLATILE = "isvolatile"; public static final String VOLUME_ID = "volumeid"; + public static final String VOLUMES = "volumes"; public static final String ZONE = "zone"; public static final String ZONE_ID = "zoneid"; public static final String ZONE_NAME = "zonename"; @@ -531,6 +537,7 @@ public class ApiConstants { public static final String REQUIRED = "required"; public static final String RESTART_REQUIRED = "restartrequired"; public static final String ALLOW_USER_CREATE_PROJECTS = "allowusercreateprojects"; + public static final String ALLOW_USER_DRIVEN_BACKUPS = "allowuserdrivenbackups"; public static final String CONSERVE_MODE = "conservemode"; public static final String TRAFFIC_TYPE_IMPLEMENTOR = "traffictypeimplementor"; public static final String KEYWORD = "keyword"; @@ -780,7 +787,7 @@ public class ApiConstants { } public enum VMDetails { - all, group, nics, stats, secgrp, tmpl, servoff, diskoff, iso, volume, min, affgrp; + all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp; } public enum DomainDetails { diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java new file mode 100644 index 00000000000..0aa8366bcd5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java @@ -0,0 +1,49 @@ +// 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; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.context.CallContext; + +public abstract class BaseBackupListCmd extends BaseListCmd { + + protected void setupResponseBackupOfferingsList(final List offerings, final Integer count) { + final ListResponse response = new ListResponse<>(); + final List responses = new ArrayList<>(); + for (final BackupOffering offering : offerings) { + if (offering == null) { + continue; + } + BackupOfferingResponse backupOfferingResponse = _responseGenerator.createBackupOfferingResponse(offering); + responses.add(backupOfferingResponse); + } + response.setResponses(responses, count); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 510562bf54a..57e03b35276 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -22,8 +22,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.apache.cloudstack.api.response.RouterHealthCheckResultResponse; -import org.apache.cloudstack.management.ManagementServerHost; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ApiConstants.HostDetails; @@ -36,6 +34,8 @@ import org.apache.cloudstack.api.response.AsyncJobResponse; import org.apache.cloudstack.api.response.AutoScalePolicyResponse; import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse; import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.BackupScheduleResponse; import org.apache.cloudstack.api.response.CapacityResponse; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.ConditionResponse; @@ -88,6 +88,7 @@ import org.apache.cloudstack.api.response.RemoteAccessVpnResponse; import org.apache.cloudstack.api.response.ResourceCountResponse; import org.apache.cloudstack.api.response.ResourceLimitResponse; import org.apache.cloudstack.api.response.ResourceTagResponse; +import org.apache.cloudstack.api.response.RouterHealthCheckResultResponse; import org.apache.cloudstack.api.response.SSHKeyPairResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; @@ -111,6 +112,7 @@ import org.apache.cloudstack.api.response.UpgradeRouterTemplateResponse; import org.apache.cloudstack.api.response.UsageRecordResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.api.response.VMSnapshotResponse; import org.apache.cloudstack.api.response.VirtualRouterProviderResponse; import org.apache.cloudstack.api.response.VlanIpRangeResponse; @@ -119,7 +121,11 @@ import org.apache.cloudstack.api.response.VpcOfferingResponse; import org.apache.cloudstack.api.response.VpcResponse; import org.apache.cloudstack.api.response.VpnUsersResponse; import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupSchedule; import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.management.ManagementServerHost; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerRule; import org.apache.cloudstack.region.PortableIp; import org.apache.cloudstack.region.PortableIpRange; @@ -467,6 +473,12 @@ public interface ResponseGenerator { SSHKeyPairResponse createSSHKeyPairResponse(SSHKeyPair sshkeyPair, boolean privatekey); + BackupResponse createBackupResponse(Backup backup); + + BackupScheduleResponse createBackupScheduleResponse(BackupSchedule backup); + + BackupOfferingResponse createBackupOfferingResponse(BackupOffering policy); + ManagementServerResponse createManagementResponse(ManagementServerHost mgmt); List createHealthCheckResponse(VirtualMachine router, List healthCheckResults); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteBackupOfferingCmd.java new file mode 100644 index 00000000000..a405fd6d4a7 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/DeleteBackupOfferingCmd.java @@ -0,0 +1,92 @@ +// 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.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; + +@APICommand(name = DeleteBackupOfferingCmd.APINAME, + description = "Deletes a backup offering", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin}) +public class DeleteBackupOfferingCmd extends BaseCmd { + public static final String APINAME = "deleteBackupOffering"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + //////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupOfferingResponse.class, + required = true, + description = "ID of the backup offering") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + if (backupManager.deleteBackupOffering(getId())) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Unable to remove backup offering: " + getId()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java new file mode 100644 index 00000000000..f682f4cddd1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java @@ -0,0 +1,145 @@ +// 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.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ImportBackupOfferingCmd.APINAME, + description = "Imports a backup offering using a backup provider", + responseObject = BackupOfferingResponse.class, since = "4.14.0", + authorized = {RoleType.Admin}) +public class ImportBackupOfferingCmd extends BaseAsyncCmd { + public static final String APINAME = "importBackupOffering"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + //////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "the name of the backup offering") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = true, + description = "the description of the backup offering") + private String description; + + @Parameter(name = ApiConstants.EXTERNAL_ID, + type = CommandType.STRING, + required = true, + description = "The backup offering ID (from backup provider side)") + private String externalId; + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + description = "The zone ID", required = true) + private Long zoneId; + + @Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = CommandType.BOOLEAN, + description = "Whether users are allowed to create adhoc backups and backup schedules", required = true) + private Boolean userDrivenBackups; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public String getExternalId() { + return externalId; + } + + public Long getZoneId() { + return zoneId; + } + + public String getDescription() { + return description; + } + + public Boolean getUserDrivenBackups() { + return userDrivenBackups == null ? false : userDrivenBackups; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + BackupOffering policy = backupManager.importBackupOffering(this); + if (policy != null) { + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(policy); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add a backup offering"); + } + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_IMPORT_OFFERING; + } + + @Override + public String getEventDescription() { + return "Importing backup offering: " + name + " (external ID: " + externalId + ") on zone ID " + zoneId ; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListBackupProviderOfferingsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListBackupProviderOfferingsCmd.java new file mode 100644 index 00000000000..2e5657d7d79 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListBackupProviderOfferingsCmd.java @@ -0,0 +1,94 @@ +// 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.admin.backup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseBackupListCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ListBackupProviderOfferingsCmd.APINAME, + description = "Lists external backup offerings of the provider", + responseObject = BackupOfferingResponse.class, since = "4.14.0", + authorized = {RoleType.Admin}) +public class ListBackupProviderOfferingsCmd extends BaseBackupListCmd { + public static final String APINAME = "listBackupProviderOfferings"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + required = true, description = "The zone ID") + private Long zoneId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getZoneId() { + return zoneId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + private void validateParameters() { + if (getZoneId() == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please provide a valid zone ID "); + } + } + + @Override + public void execute() throws ResourceUnavailableException, ServerApiException, ConcurrentOperationException { + validateParameters(); + try { + final List backupOfferings = backupManager.listBackupProviderOfferings(getZoneId()); + setupResponseBackupOfferingsList(backupOfferings, backupOfferings.size()); + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + RESPONSE_SUFFIX; + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListBackupProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListBackupProvidersCmd.java new file mode 100644 index 00000000000..2b4b735be2a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ListBackupProvidersCmd.java @@ -0,0 +1,98 @@ +// 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.admin.backup; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.BackupProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupProvider; + +import com.cloud.user.Account; + +@APICommand(name = ListBackupProvidersCmd.APINAME, + description = "Lists Backup and Recovery providers", + responseObject = BackupProviderResponse.class, since = "4.14.0", + authorized = {RoleType.Admin}) +public class ListBackupProvidersCmd extends BaseCmd { + public static final String APINAME = "listBackupProviders"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List Backup and Recovery provider by name") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + private void setupResponse(final List providers) { + final ListResponse response = new ListResponse<>(); + final List responses = new ArrayList<>(); + for (final BackupProvider provider : providers) { + if (provider == null || (getName() != null && !provider.getName().equals(getName()))) { + continue; + } + final BackupProviderResponse backupProviderResponse = new BackupProviderResponse(); + backupProviderResponse.setName(provider.getName()); + backupProviderResponse.setDescription(provider.getDescription()); + backupProviderResponse.setObjectName("providers"); + responses.add(backupProviderResponse); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public void execute() { + List providers = backupManager.listBackupProviders(); + setupResponse(providers); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java new file mode 100644 index 00000000000..b5c09867b67 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/AssignVirtualMachineToBackupOfferingCmd.java @@ -0,0 +1,122 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; + +@APICommand(name = AssignVirtualMachineToBackupOfferingCmd.APINAME, + description = "Assigns a VM to a backup offering", + responseObject = BackupResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class AssignVirtualMachineToBackupOfferingCmd extends BaseAsyncCmd { + public static final String APINAME = "assignVirtualMachineToBackupOffering"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the virtual machine") + private Long vmId; + + @Parameter(name = ApiConstants.BACKUP_OFFERING_ID, + type = CommandType.UUID, + entityType = BackupOfferingResponse.class, + required = true, + description = "ID of the backup offering") + private Long offeringId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + public Long getOfferingId() { + return offeringId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.assignVMToBackupOffering(getVmId(), getOfferingId()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add VM to backup offering"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_OFFERING_ASSIGN; + } + + @Override + public String getEventDescription() { + return "Assigning VM to backup offering ID: " + offeringId; + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java new file mode 100644 index 00000000000..db7a2763ef2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupCmd.java @@ -0,0 +1,126 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = CreateBackupCmd.APINAME, + description = "Create VM backup", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateBackupCmd extends BaseAsyncCreateCmd { + public static final String APINAME = "createBackup"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.createBackup(getVmId()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("Error while creating backup of VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.Backup; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_CREATE; + } + + @Override + public String getEventDescription() { + return "Creating backup for VM " + vmId; + } + + @Override + public void create() throws ResourceAllocationException { + } + + @Override + public Long getEntityId() { + return vmId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java new file mode 100644 index 00000000000..e10386de987 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/CreateBackupScheduleCmd.java @@ -0,0 +1,128 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupSchedule; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.utils.DateUtil; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = CreateBackupScheduleCmd.APINAME, + description = "Creates a user-defined VM backup schedule", + responseObject = BackupResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateBackupScheduleCmd extends BaseCmd { + public static final String APINAME = "createBackupSchedule"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM for which schedule is to be defined") + private Long vmId; + + @Parameter(name = ApiConstants.INTERVAL_TYPE, + type = CommandType.STRING, + required = true, + description = "valid values are HOURLY, DAILY, WEEKLY, and MONTHLY") + private String intervalType; + + @Parameter(name = ApiConstants.SCHEDULE, + type = CommandType.STRING, + required = true, + description = "custom backup schedule, the format is:" + + "for HOURLY MM*, for DAILY MM:HH*, for WEEKLY MM:HH:DD (1-7)*, for MONTHLY MM:HH:DD (1-28)") + private String schedule; + + @Parameter(name = ApiConstants.TIMEZONE, + type = CommandType.STRING, + required = true, + description = "Specifies a timezone for this command. For more information on the timezone parameter, see TimeZone Format.") + private String timezone; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + public DateUtil.IntervalType getIntervalType() { + return DateUtil.IntervalType.getIntervalType(intervalType); + } + + public String getSchedule() { + return schedule; + } + + public String getTimezone() { + return timezone; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ServerApiException { + try { + BackupSchedule schedule = backupManager.configureBackupSchedule(this); + if (schedule != null) { + BackupScheduleResponse response = _responseGenerator.createBackupScheduleResponse(schedule); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("Error while creating backup schedule of VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.java new file mode 100644 index 00000000000..32344d8992b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupCmd.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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = DeleteBackupCmd.APINAME, + description = "Delete VM backup", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteBackupCmd extends BaseAsyncCmd { + public static final String APINAME = "deleteBackup"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + required = true, + description = "id of the VM backup") + private Long backupId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return backupId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.deleteBackup(backupId); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("Error while deleting backup of VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_DELETE; + } + + @Override + public String getEventDescription() { + return "Deleting backup ID " + backupId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java new file mode 100644 index 00000000000..1c7b65c8edc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java @@ -0,0 +1,99 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = DeleteBackupScheduleCmd.APINAME, + description = "Deletes the backup schedule of a VM", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteBackupScheduleCmd extends BaseCmd { + public static final String APINAME = "deleteBackupSchedule"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.deleteBackupSchedule(getVmId()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("Failed to delete VM backup schedule"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupOfferingsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupOfferingsCmd.java new file mode 100644 index 00000000000..e745a6ba66d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupOfferingsCmd.java @@ -0,0 +1,96 @@ +// 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.backup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseBackupListCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ListBackupOfferingsCmd.APINAME, + description = "Lists backup offerings", + responseObject = BackupOfferingResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListBackupOfferingsCmd extends BaseBackupListCmd { + public static final String APINAME = "listBackupOfferings"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID, entityType = BackupOfferingResponse.class, + description = "The backup offering ID") + private Long offeringId; + + @Parameter(name = ApiConstants.ZONE_ID, type = BaseCmd.CommandType.UUID, entityType = ZoneResponse.class, + description = "The zone ID") + private Long zoneId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getZoneId() { + return zoneId; + } + + public Long getOfferingId() { + return offeringId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, ServerApiException, ConcurrentOperationException { + try { + Pair, Integer> result = backupManager.listBackupOfferings(this); + setupResponseBackupOfferingsList(result.first(), result.second()); + } catch (InvalidParameterValueException e) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage()); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + RESPONSE_SUFFIX; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java new file mode 100644 index 00000000000..4068dc295dc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupScheduleCmd.java @@ -0,0 +1,100 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupSchedule; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ListBackupScheduleCmd.APINAME, + description = "List backup schedule of a VM", + responseObject = BackupScheduleResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListBackupScheduleCmd extends BaseCmd { + public static final String APINAME = "listBackupSchedule"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the VM") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try{ + BackupSchedule schedule = backupManager.listBackupSchedule(getVmId()); + if (schedule != null) { + BackupScheduleResponse response = _responseGenerator.createBackupScheduleResponse(schedule); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("No backup schedule exists for the VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java new file mode 100644 index 00000000000..1e1e7312897 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/ListBackupsCmd.java @@ -0,0 +1,135 @@ +// 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.backup; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.Pair; + +@APICommand(name = ListBackupsCmd.APINAME, + description = "Lists VM backups", + responseObject = BackupResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListBackupsCmd extends BaseListProjectAndAccountResourcesCmd { + public static final String APINAME = "listBackups"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + description = "id of the backup") + private Long id; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + description = "id of the VM") + private Long vmId; + + @Parameter(name = ApiConstants.ZONE_ID, + type = CommandType.UUID, + entityType = ZoneResponse.class, + description = "list backups by zone id") + private Long zoneId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public Long getVmId() { + return vmId; + } + + public Long getZoneId() { + return zoneId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + protected void setupResponseBackupList(final List backups, final Integer count) { + final List responses = new ArrayList<>(); + for (Backup backup : backups) { + if (backup == null) { + continue; + } + BackupResponse backupResponse = _responseGenerator.createBackupResponse(backup); + responses.add(backupResponse); + } + final ListResponse response = new ListResponse<>(); + response.setResponses(responses, count); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try{ + Pair, Integer> result = backupManager.listBackups(this); + setupResponseBackupList(result.first(), result.second()); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java new file mode 100644 index 00000000000..28f03f8c853 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RemoveVirtualMachineFromBackupOfferingCmd.java @@ -0,0 +1,118 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; + +@APICommand(name = RemoveVirtualMachineFromBackupOfferingCmd.APINAME, + description = "Removes a VM from any existing backup offering", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class RemoveVirtualMachineFromBackupOfferingCmd extends BaseAsyncCmd { + public static final String APINAME = "removeVirtualMachineFromBackupOffering"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "ID of the virtual machine") + private Long vmId; + + @Parameter(name = ApiConstants.FORCED, + type = CommandType.BOOLEAN, + description = "Whether to force remove VM from the backup offering that may also delete VM backups.") + private Boolean forced; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + public boolean getForced() { + return forced == null ? false : forced; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.removeVMFromBackupOffering(getVmId(), getForced()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove VM from backup offering"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_OFFERING_REMOVE; + } + + @Override + public String getEventDescription() { + return "Removing VM ID" + vmId + " from backup offering"; + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.java new file mode 100644 index 00000000000..62fa9a17b77 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreBackupCmd.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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = RestoreBackupCmd.APINAME, + description = "Restores an existing stopped or deleted VM using a VM backup", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class RestoreBackupCmd extends BaseAsyncCmd { + public static final String APINAME = "restoreBackup"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + required = true, + description = "ID of the backup") + private Long backupId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getBackupId() { + return backupId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.restoreBackup(backupId); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("Error while restoring VM from backup"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_RESTORE; + } + + @Override + public String getEventDescription() { + return "Restoring VM from backup: " + backupId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java new file mode 100644 index 00000000000..b5966a737df --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/RestoreVolumeFromBackupAndAttachToVMCmd.java @@ -0,0 +1,133 @@ +// 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.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = RestoreVolumeFromBackupAndAttachToVMCmd.APINAME, + description = "Restore and attach a backed up volume to VM", + responseObject = SuccessResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class RestoreVolumeFromBackupAndAttachToVMCmd extends BaseAsyncCmd { + public static final String APINAME = "restoreVolumeFromBackupAndAttachToVM"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.BACKUP_ID, + type = CommandType.UUID, + entityType = BackupResponse.class, + required = true, + description = "ID of the VM backup") + private Long backupId; + + @Parameter(name = ApiConstants.VOLUME_ID, + type = CommandType.STRING, + required = true, + description = "ID of the volume backed up") + private String volumeUuid; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, + type = CommandType.UUID, + entityType = UserVmResponse.class, + required = true, + description = "id of the VM where to attach the restored volume") + private Long vmId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getVolumeUuid() { + return volumeUuid; + } + + public Long getVmId() { + return vmId; + } + + public Long getBackupId() { + return backupId; + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = backupManager.restoreBackupVolumeAndAttachToVM(volumeUuid, backupId, vmId); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new CloudRuntimeException("Error restoring volume and attaching to VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_BACKUP_RESTORE_VOLUME_TO_VM; + } + + @Override + public String getEventDescription() { + return "Restoring volume "+ volumeUuid + " from backup " + backupId + " and attaching it to VM " + vmId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/UpdateBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/UpdateBackupScheduleCmd.java new file mode 100644 index 00000000000..5a02cf9ac78 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/UpdateBackupScheduleCmd.java @@ -0,0 +1,30 @@ +// 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.backup; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.response.BackupResponse; + +@APICommand(name = UpdateBackupScheduleCmd.APINAME, + description = "Updates a user-defined VM backup schedule", + responseObject = BackupResponse.class, since = "4.14.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateBackupScheduleCmd extends CreateBackupScheduleCmd { + public static final String APINAME = "updateBackupSchedule"; +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java new file mode 100644 index 00000000000..480ebcfb13d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java @@ -0,0 +1,95 @@ +// 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.response; + +import java.util.Date; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.BackupOffering; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = BackupOffering.class) +public class BackupOfferingResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the backup offering") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "name for the backup offering") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "description for the backup offering") + private String description; + + @SerializedName(ApiConstants.EXTERNAL_ID) + @Param(description = "external ID on the provider side") + private String externalId; + + @SerializedName(ApiConstants.ALLOW_USER_DRIVEN_BACKUPS) + @Param(description = "whether offering allows user driven ad-hoc/scheduled backups") + private Boolean userDrivenBackups; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "zone ID") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) + @Param(description = "zone name") + private String zoneName; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the date this backup offering was created") + private Date created; + + public void setId(String id) { + this.id = id; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setUserDrivenBackups(Boolean userDrivenBackups) { + this.userDrivenBackups = userDrivenBackups; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupProviderResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupProviderResponse.java new file mode 100644 index 00000000000..5227d850887 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupProviderResponse.java @@ -0,0 +1,53 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.BackupProvider; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(BackupProvider.class) +public class BackupProviderResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) + @Param(description = "the CA service provider name") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the description of the CA service provider") + private String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java new file mode 100644 index 00000000000..d0c8e588450 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java @@ -0,0 +1,246 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.Backup; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = Backup.class) +public class BackupResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the VM backup") + private String id; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_ID) + @Param(description = "ID of the VM") + private String vmId; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME) + @Param(description = "name of the VM") + private String vmName; + + @SerializedName(ApiConstants.EXTERNAL_ID) + @Param(description = "external backup id") + private String externalId; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "backup type") + private String type; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "backup date") + private String date; + + @SerializedName(ApiConstants.SIZE) + @Param(description = "backup size in bytes") + private Long size; + + @SerializedName(ApiConstants.VIRTUAL_SIZE) + @Param(description = "backup protected (virtual) size in bytes") + private Long protectedSize; + + @SerializedName(ApiConstants.STATUS) + @Param(description = "backup status") + private Backup.Status status; + + @SerializedName(ApiConstants.VOLUMES) + @Param(description = "backed up volumes") + private String volumes; + + @SerializedName(ApiConstants.BACKUP_OFFERING_ID) + @Param(description = "backup offering id") + private String backupOfferingId; + + @SerializedName(ApiConstants.BACKUP_OFFERING_NAME) + @Param(description = "backup offering name") + private String backupOfferingName; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "account id") + private String accountId; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "account name") + private String account; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "domain id") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "domain name") + private String domain; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "zone id") + private String zoneId; + + @SerializedName(ApiConstants.ZONE) + @Param(description = "zone name") + private String zone; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getVmId() { + return vmId; + } + + public void setVmId(String vmId) { + this.vmId = vmId; + } + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public Long getProtectedSize() { + return protectedSize; + } + + public void setProtectedSize(Long protectedSize) { + this.protectedSize = protectedSize; + } + + public Backup.Status getStatus() { + return status; + } + + public void setStatus(Backup.Status status) { + this.status = status; + } + + public String getVolumes() { + return volumes; + } + + public void setVolumes(String volumes) { + this.volumes = volumes; + } + + public String getBackupOfferingId() { + return backupOfferingId; + } + + public void setBackupOfferingId(String backupOfferingId) { + this.backupOfferingId = backupOfferingId; + } + + public String getBackupOffering() { + return backupOfferingName; + } + + public void setBackupOffering(String backupOfferingName) { + this.backupOfferingName = backupOfferingName; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + + public String getDomainId() { + return domainId; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getZoneId() { + return zoneId; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public String getZone() { + return zone; + } + + public void setZone(String zone) { + this.zone = zone; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupRestorePointResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupRestorePointResponse.java new file mode 100644 index 00000000000..afb3e9ffc5f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupRestorePointResponse.java @@ -0,0 +1,66 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.Backup; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = Backup.RestorePoint.class) +public class BackupRestorePointResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "external id of the restore point") + private String id; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "created time") + private String created; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "restore point type") + private String type; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java new file mode 100644 index 00000000000..ba44f1e024f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java @@ -0,0 +1,91 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.backup.BackupSchedule; + +import com.cloud.serializer.Param; +import com.cloud.utils.DateUtil; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = BackupSchedule.class) +public class BackupScheduleResponse extends BaseResponse { + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME) + @Param(description = "name of the VM") + private String vmName; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_ID) + @Param(description = "ID of the VM") + private String vmId; + + @SerializedName("schedule") + @Param(description = "time the backup is scheduled to be taken.") + private String schedule; + + @SerializedName("intervaltype") + @Param(description = "the interval type of the backup schedule") + private DateUtil.IntervalType intervalType; + + @SerializedName("timezone") + @Param(description = "the time zone of the backup schedule") + private String timezone; + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public String getVmId() { + return vmId; + } + + public void setVmId(String vmId) { + this.vmId = vmId; + } + + public String getSchedule() { + return schedule; + } + + public void setSchedule(String schedule) { + this.schedule = schedule; + } + + public DateUtil.IntervalType getIntervalType() { + return intervalType; + } + + public void setIntervalType(DateUtil.IntervalType intervalType) { + this.intervalType = intervalType; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 8a2f1a169d6..ca3ca872943 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -156,6 +156,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "the name of the disk offering of the virtual machine", since = "4.4") private String diskOfferingName; + @SerializedName(ApiConstants.BACKUP_OFFERING_ID) + @Param(description = "the ID of the backup offering of the virtual machine", since = "4.14") + private String backupOfferingId; + + @SerializedName(ApiConstants.BACKUP_OFFERING_NAME) + @Param(description = "the name of the backup offering of the virtual machine", since = "4.14") + private String backupOfferingName; + @SerializedName("forvirtualnetwork") @Param(description = "the virtual network for the service offering") private Boolean forVirtualNetwork; @@ -439,6 +447,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co return diskOfferingName; } + public String getBackupOfferingId() { + return backupOfferingId; + } + + public String getBackupOfferingName() { + return backupOfferingName; + } + public Boolean getForVirtualNetwork() { return forVirtualNetwork; } @@ -697,6 +713,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co this.diskOfferingName = diskOfferingName; } + public void setBackupOfferingId(String backupOfferingId) { + this.backupOfferingId = backupOfferingId; + } + + public void setBackupOfferingName(String backupOfferingName) { + this.backupOfferingName = backupOfferingName; + } + public void setCpuNumber(Integer cpuNumber) { this.cpuNumber = cpuNumber; } diff --git a/api/src/main/java/org/apache/cloudstack/backup/Backup.java b/api/src/main/java/org/apache/cloudstack/backup/Backup.java new file mode 100644 index 00000000000..e6aa238ec9a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/Backup.java @@ -0,0 +1,142 @@ +//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 +//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.backup; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import com.cloud.storage.Volume; +import com.cloud.utils.StringUtils; + +public interface Backup extends ControlledEntity, InternalIdentity, Identity { + + enum Status { + Allocated, Queued, BackingUp, BackedUp, Error, Failed, Restoring, Removed, Expunged + } + + class Metric { + private Long backupSize = 0L; + private Long dataSize = 0L; + + public Metric(final Long backupSize, final Long dataSize) { + this.backupSize = backupSize; + this.dataSize = dataSize; + } + + public Long getBackupSize() { + return backupSize; + } + + public Long getDataSize() { + return dataSize; + } + + public void setBackupSize(Long backupSize) { + this.backupSize = backupSize; + } + + public void setDataSize(Long dataSize) { + this.dataSize = dataSize; + } + } + + class RestorePoint { + private String id; + private String created; + private String type; + + public RestorePoint(String id, String created, String type) { + this.id = id; + this.created = created; + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } + + class VolumeInfo { + private String uuid; + private Volume.Type type; + private Long size; + private String path; + + public VolumeInfo(String uuid, String path, Volume.Type type, Long size) { + this.uuid = uuid; + this.type = type; + this.size = size; + this.path = path; + } + + public String getUuid() { + return uuid; + } + + public Volume.Type getType() { + return type; + } + + public void setType(Volume.Type type) { + this.type = type; + } + + public String getPath() { + return path; + } + + public Long getSize() { + return size; + } + + @Override + public String toString() { + return StringUtils.join(":", uuid, path, type, size); + } + } + + long getVmId(); + String getExternalId(); + String getType(); + String getDate(); + Backup.Status getStatus(); + Long getSize(); + Long getProtectedSize(); + long getZoneId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java new file mode 100644 index 00000000000..7c9d3b6d0a1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -0,0 +1,140 @@ +// 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.backup; + +import java.util.List; + +import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.utils.Pair; +import com.cloud.utils.component.Manager; +import com.cloud.utils.component.PluggableService; + +/** + * Backup and Recover Manager Interface + */ +public interface BackupManager extends BackupService, Configurable, PluggableService, Manager { + + ConfigKey BackupFrameworkEnabled = new ConfigKey<>("Advanced", Boolean.class, + "backup.framework.enabled", + "false", + "Is backup and recovery framework enabled.", true, ConfigKey.Scope.Zone); + + ConfigKey BackupProviderPlugin = new ConfigKey<>("Advanced", String.class, + "backup.framework.provider.plugin", + "dummy", + "The backup and recovery provider plugin.", true, ConfigKey.Scope.Zone); + + ConfigKey BackupSyncPollingInterval = new ConfigKey<>("Advanced", Long.class, + "backup.framework.sync.interval", + "300", + "The backup and recovery background sync task polling interval in seconds.", true); + + /** + * List backup provider offerings + * @param zoneId zone id + */ + List listBackupProviderOfferings(final Long zoneId); + + /** + * Add a new Backup and Recovery policy to CloudStack by mapping an existing external backup offering to a name and description + * @param cmd import backup offering cmd + */ + BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd); + + /** + * List backup offerings + * @param ListBackupOfferingsCmd API cmd + */ + Pair, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd); + + /** + * Deletes a backup offering + */ + boolean deleteBackupOffering(final Long policyId); + + /** + * Assigns a VM to a backup offering + * @param vmId + * @param offeringId + * @return + */ + boolean assignVMToBackupOffering(final Long vmId, final Long offeringId); + + /** + * Removes a VM from a backup offering + * @param vmId + * @param forced + * @return + */ + boolean removeVMFromBackupOffering(final Long vmId, final boolean forced); + + /** + * Creates or Updates a VM backup schedule + * @param cmd + * @return + */ + BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd); + + /** + * Lists VM backup schedule for a VM + * @param vmId + * @return + */ + BackupSchedule listBackupSchedule(Long vmId); + + /** + * Deletes VM backup schedule for a VM + * @param vmId + * @return + */ + boolean deleteBackupSchedule(Long vmId); + + /** + * Creates backup of a VM + * @param vmId Virtual Machine ID + * @return returns operation success + */ + boolean createBackup(final Long vmId); + + /** + * List existing backups for a VM + */ + Pair, Integer> listBackups(final ListBackupsCmd cmd); + + /** + * Restore a full VM from backup + */ + boolean restoreBackup(final Long backupId); + + /** + * Restore a backed up volume and attach it to a VM + */ + boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId) throws Exception; + + /** + * Deletes a backup + * @return returns operation success + */ + boolean deleteBackup(final Long backupId); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupOffering.java b/api/src/main/java/org/apache/cloudstack/backup/BackupOffering.java new file mode 100644 index 00000000000..156c9cdbb10 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupOffering.java @@ -0,0 +1,32 @@ +//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 +//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.backup; + +import java.util.Date; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface BackupOffering extends InternalIdentity, Identity { + String getExternalId(); + String getName(); + String getDescription(); + long getZoneId(); + boolean isUserDrivenBackupAllowed(); + String getProvider(); + Date getCreated(); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.java new file mode 100644 index 00000000000..ff05a38eeb6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupProvider.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 +//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.backup; + +import java.util.List; +import java.util.Map; + +import com.cloud.utils.Pair; +import com.cloud.vm.VirtualMachine; + +public interface BackupProvider { + + /** + * Returns the unique name of the provider + * @return returns provider name + */ + String getName(); + + /** + * Returns description about the backup and recovery provider plugin + * @return returns description + */ + String getDescription(); + + /** + * Returns the list of existing backup policies on the provider + * @return backup policies list + */ + List listBackupOfferings(Long zoneId); + + /** + * True if a backup offering exists on the backup provider + */ + boolean isValidProviderOffering(Long zoneId, String uuid); + + /** + * Assign a VM to a backup offering or policy + * @param vm + * @param backup + * @param policy + * @return + */ + boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering); + + /** + * Removes a VM from a backup offering or policy + * @param vm + * @return + */ + boolean removeVMFromBackupOffering(VirtualMachine vm); + + /** + * Whether the provide will delete backups on removal of VM from the offfering + * @return boolean result + */ + boolean willDeleteBackupsOnOfferingRemoval(); + + /** + * Starts and creates an adhoc backup process + * for a previously registered VM backup + * @param backup + * @return + */ + boolean takeBackup(VirtualMachine vm); + + /** + * Delete an existing backup + * @param backup + * @return + */ + boolean deleteBackup(Backup backup); + + /** + * Restore VM from backup + */ + boolean restoreVMFromBackup(VirtualMachine vm, Backup backup); + + /** + * Restore a volume from a backup + */ + Pair restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid); + + /** + * Returns backup metrics for a list of VMs in a zone + * @param zoneId + * @param vms + * @return + */ + Map getBackupMetrics(Long zoneId, List vms); + + /** + * This method should reconcile and create backup entries for any backups created out-of-band + * @param vm + * @param metric + */ + void syncBackups(VirtualMachine vm, Backup.Metric metric); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java new file mode 100644 index 00000000000..d81dd731b1f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java @@ -0,0 +1,33 @@ +// 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.backup; + +import java.util.Date; + +import org.apache.cloudstack.api.InternalIdentity; + +import com.cloud.utils.DateUtil; + +public interface BackupSchedule extends InternalIdentity { + Long getVmId(); + DateUtil.IntervalType getScheduleType(); + String getSchedule(); + String getTimezone(); + Date getScheduledTimestamp(); + Long getAsyncJobId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupService.java b/api/src/main/java/org/apache/cloudstack/backup/BackupService.java new file mode 100644 index 00000000000..d4beb629fe0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupService.java @@ -0,0 +1,37 @@ +//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 +//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.backup; + +import java.util.List; + +/** + * Backup and Recovery Services + */ +public interface BackupService { + /** + * Lists backup and recovery provider plugins + * @return list of providers + */ + List listBackupProviders(); + + /** + * Find backup provider by zone ID + * @param zoneId zone id + * @return backup provider + */ + BackupProvider getBackupProvider(final Long zoneId); +} diff --git a/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java b/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java index d0b700697f3..48cff3076fd 100644 --- a/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java +++ b/api/src/main/java/org/apache/cloudstack/usage/UsageTypes.java @@ -44,6 +44,7 @@ public class UsageTypes { public static final int VM_SNAPSHOT = 25; public static final int VOLUME_SECONDARY = 26; public static final int VM_SNAPSHOT_ON_PRIMARY = 27; + public static final int BACKUP = 28; public static List listUsageTypes() { List responseList = new ArrayList(); @@ -68,6 +69,7 @@ public class UsageTypes { responseList.add(new UsageTypeResponse(VM_SNAPSHOT, "VM Snapshot storage usage")); responseList.add(new UsageTypeResponse(VOLUME_SECONDARY, "Volume on secondary storage usage")); responseList.add(new UsageTypeResponse(VM_SNAPSHOT_ON_PRIMARY, "VM Snapshot on primary storage usage")); + responseList.add(new UsageTypeResponse(BACKUP, "Backup storage usage")); return responseList; } } diff --git a/client/pom.xml b/client/pom.xml index 507101bf631..29ecdece278 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -478,6 +478,11 @@ cloud-plugin-integrations-prometheus-exporter ${project.version} + + org.apache.cloudstack + cloud-plugin-backup-dummy + ${project.version} + @@ -768,6 +773,33 @@ META-INF/spring.schemas + + META-INF/services/com.sun.tools.xjc.Plugin + + + META-INF/cxf/cxf.extension + + + META-INF/extensions.xml + + + META-INF/cxf/extensions.xml + + + META-INF/cxf/bus-extensions.txt + + + META-INF/cxf/bus-extensions.xml + + + META-INF/wsdl.plugin.xml + + + META-INF/tools.service.validator.xml + + + META-INF/cxf/java2wsbeans.xml + @@ -902,6 +934,16 @@ cloud-plugin-network-cisco-vnmc ${project.version} + + org.apache.cloudstack + cloud-plugin-api-vmware-sioc + ${project.version} + + + org.apache.cloudstack + cloud-plugin-backup-veeam + ${project.version} + @@ -919,21 +961,6 @@ - - vmwaresioc - - - noredist - - - - - org.apache.cloudstack - cloud-plugin-api-vmware-sioc - ${project.version} - - - quickcloud diff --git a/core/src/main/resources/META-INF/cloudstack/backup/module.properties b/core/src/main/resources/META-INF/cloudstack/backup/module.properties new file mode 100644 index 00000000000..b85b65ceeea --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/backup/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=backup +parent=backend diff --git a/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml new file mode 100644 index 00000000000..175d45e2675 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/backup/spring-core-lifecycle-backup-context-inheritable.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index 2569d8b6487..affd441fb6c 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -328,4 +328,8 @@ class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> + + + diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java index 1dd7941fa57..a84e4d575f4 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDao.java @@ -121,6 +121,8 @@ public interface NetworkDao extends GenericDao, StateDao listNetworkVO(List idset); + NetworkVO findByVlan(String vlan); + List listByAccountIdNetworkName(long accountId, String name); List listByPhysicalNetworkPvlan(long physicalNetworkId, String broadcastUri, Network.PVlanType pVlanType); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java index 711da1fa7f7..eeee3d12c65 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDaoImpl.java @@ -718,6 +718,14 @@ public class NetworkDaoImpl extends GenericDaoBaseimplements Ne return this.search(sc_2, searchFilter_2); } + @Override + public NetworkVO findByVlan(String vlan) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("broadcastType", BroadcastDomainType.Vlan); + sc.setParameters("broadcastUri", BroadcastDomainType.Vlan.toUri(vlan)); + return findOneBy(sc); + } + @Override public List listByAccountIdNetworkName(final long accountId, final String name) { final SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java index 8ac44b99c1d..623179c2d8e 100644 --- a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java @@ -51,4 +51,6 @@ public interface ServiceOfferingDao extends GenericDao ServiceOfferingVO getComputeOffering(ServiceOfferingVO serviceOffering, Map customParameters); ServiceOfferingVO findDefaultSystemOffering(String offeringName, Boolean useLocalStorage); + + List listPublicByCpuAndMemory(Integer cpus, Integer memory); } diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java index 76e867b8f53..14400516d54 100644 --- a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java @@ -51,6 +51,7 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase UniqueNameSearch; protected final SearchBuilder ServiceOfferingsByKeywordSearch; + protected final SearchBuilder PublicCpuRamSearch; public ServiceOfferingDaoImpl() { super(); @@ -64,6 +65,12 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase listPublicByCpuAndMemory(Integer cpus, Integer memory) { + SearchCriteria sc = PublicCpuRamSearch.create(); + sc.setParameters("cpu", cpus); + sc.setParameters("ram", memory); + sc.setParameters("system_use", false); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java index 89e2c832836..3305752459d 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDao.java @@ -19,6 +19,7 @@ package com.cloud.storage.dao; import java.util.List; import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage; import com.cloud.utils.db.GenericDao; public interface DiskOfferingDao extends GenericDao { @@ -31,4 +32,6 @@ public interface DiskOfferingDao extends GenericDao { DiskOfferingVO persistDeafultDiskOffering(DiskOfferingVO offering); + List listAllBySizeAndProvisioningType(long size, Storage.ProvisioningType provisioningType); + } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java index d93a05200f6..b9fa10c1236 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/DiskOfferingDaoImpl.java @@ -16,6 +16,10 @@ // under the License. package com.cloud.storage.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -27,12 +31,15 @@ import org.springframework.stereotype.Component; import com.cloud.offering.DiskOffering.Type; import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.Storage; import com.cloud.utils.db.Attribute; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.CloudRuntimeException; @Component public class DiskOfferingDaoImpl extends GenericDaoBase implements DiskOfferingDao { @@ -43,7 +50,11 @@ public class DiskOfferingDaoImpl extends GenericDaoBase im private final SearchBuilder PrivateDiskOfferingSearch; private final SearchBuilder PublicDiskOfferingSearch; protected final SearchBuilder UniqueNameSearch; + private final String SizeDiskOfferingSearch = "SELECT * FROM disk_offering WHERE " + + "disk_size = ? AND provisioning_type = ? AND removed IS NULL"; + private final Attribute _typeAttr; + protected final static long GB_UNIT_BYTES = 1024 * 1024 * 1024; protected DiskOfferingDaoImpl() { PrivateDiskOfferingSearch = createSearchBuilder(); @@ -132,6 +143,36 @@ public class DiskOfferingDaoImpl extends GenericDaoBase im } } + protected long getClosestDiskSizeInGB(long sizeInBytes) { + if (sizeInBytes < 0) { + throw new CloudRuntimeException("Disk size should be greater than 0 bytes, received: " + sizeInBytes + " bytes"); + } + return (long) Math.ceil(1.0 * sizeInBytes / GB_UNIT_BYTES); + } + + @Override + public List listAllBySizeAndProvisioningType(long size, Storage.ProvisioningType provisioningType) { + StringBuilder sql = new StringBuilder(SizeDiskOfferingSearch); + TransactionLegacy txn = TransactionLegacy.currentTxn(); + List offerings = new ArrayList<>(); + try(PreparedStatement pstmt = txn.prepareStatement(sql.toString());){ + if(pstmt != null) { + pstmt.setLong(1, size); + pstmt.setString(2, provisioningType.toString()); + try(ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + offerings.add(toEntityBean(rs, false)); + } + } catch (SQLException e) { + throw new CloudRuntimeException("Exception while listing disk offerings by size: " + e.getMessage(), e); + } + } + return offerings; + } catch (SQLException e) { + throw new CloudRuntimeException("Exception while listing disk offerings by size: " + e.getMessage(), e); + } + } + @Override public boolean remove(Long id) { DiskOfferingVO diskOffering = createForUpdate(); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDao.java index 6216ef77ca2..05afad67655 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDao.java @@ -48,4 +48,8 @@ public interface VMTemplatePoolDao extends GenericDao listByTemplatePath(String templatePath); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java index bb3985f6dc7..32874701128 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplatePoolDaoImpl.java @@ -59,6 +59,7 @@ public class VMTemplatePoolDaoImpl extends GenericDaoBase TemplateStatesSearch; protected final SearchBuilder TemplatePoolStateSearch; protected final SearchBuilder updateStateSearch; + protected final SearchBuilder templatePathSearch; protected static final String UPDATE_TEMPLATE_HOST_REF = "UPDATE template_spool_ref SET download_state = ?, download_pct= ?, last_updated = ? " + ", error_str = ?, local_path = ?, job_id = ? " + "WHERE pool_id = ? and template_id = ?"; @@ -114,6 +115,12 @@ public class VMTemplatePoolDaoImpl extends GenericDaoBase sc = templatePathSearch.create(); + sc.setParameters("local_path", path); + sc.setParameters("install_path", path); + sc.setParameters("pool_id", poolId); + return findOneBy(sc); + } + + @Override + public List listByTemplatePath(String templatePath) { + SearchCriteria sc = templatePathSearch.create(); + sc.setParameters("local_path", templatePath); + sc.setParameters("install_path", templatePath); + return listBy(sc); + } + @Override public boolean updateState(State currentState, Event event, State nextState, DataObjectInStore vo, Object data) { VMTemplateStoragePoolVO templatePool = (VMTemplateStoragePoolVO)vo; diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index 2741307c054..b410f48d113 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -46,6 +46,8 @@ public interface VolumeDao extends GenericDao, StateDao findByInstanceAndType(long id, Volume.Type vType); + List findIncludingRemovedByInstanceAndType(long id, Volume.Type vType); + List findByInstanceIdAndPoolId(long instanceId, long poolId); List findByInstanceIdDestroyed(long vmId); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 974d8e6f9ce..12e658a65a6 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -190,6 +190,16 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol return listBy(sc); } + @Override + public List findIncludingRemovedByInstanceAndType(long id, Type vType) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("instanceId", id); + if (vType != null) { + sc.setParameters("vType", vType.toString()); + } + return listIncludingRemovedBy(sc); + } + @Override public List findByInstanceIdDestroyed(long vmId) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/usage/UsageBackupVO.java b/engine/schema/src/main/java/com/cloud/usage/UsageBackupVO.java new file mode 100644 index 00000000000..43e3974aa0e --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/usage/UsageBackupVO.java @@ -0,0 +1,172 @@ +// 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 com.cloud.usage; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "usage_backup") +public class UsageBackupVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "backup_offering_id") + private long backupOfferingId; + + @Column(name = "size") + private long size; + + @Column(name = "protected_size") + private long protectedSize; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = null; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + protected UsageBackupVO() { + } + + public UsageBackupVO(long zoneId, long accountId, long domainId, long vmId, long backupOfferingId, Date created) { + this.zoneId = zoneId; + this.accountId = accountId; + this.domainId = domainId; + this.vmId = vmId; + this.backupOfferingId = backupOfferingId; + this.created = created; + } + + public UsageBackupVO(long id, long zoneId, long accountId, long domainId, long vmId, long backupOfferingId, long size, long protectedSize, Date created, Date removed) { + this.id = id; + this.zoneId = zoneId; + this.accountId = accountId; + this.domainId = domainId; + this.vmId = vmId; + this.backupOfferingId = backupOfferingId; + this.size = size; + this.protectedSize = protectedSize; + this.created = created; + this.removed = removed; + } + + @Override + public long getId() { + return id; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public long getVmId() { + return vmId; + } + + public void setVmId(long vmId) { + this.vmId = vmId; + } + + public long getBackupOfferingId() { + return backupOfferingId; + } + + public void setBackupOfferingId(long backupOfferingId) { + this.backupOfferingId = backupOfferingId; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getProtectedSize() { + return protectedSize; + } + + public void setProtectedSize(long protectedSize) { + this.protectedSize = protectedSize; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } +} diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDao.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDao.java new file mode 100644 index 00000000000..fb93c01a122 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDao.java @@ -0,0 +1,33 @@ +// 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 com.cloud.usage.dao; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.backup.Backup; + +import com.cloud.usage.UsageBackupVO; +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.VirtualMachine; + +public interface UsageBackupDao extends GenericDao { + void updateMetrics(VirtualMachine vm, Backup.Metric metric); + void removeUsage(Long accountId, Long zoneId, Long backupId); + List getUsageRecords(Long accountId, Date startDate, Date endDate); +} diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java new file mode 100644 index 00000000000..35b86bc082f --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageBackupDaoImpl.java @@ -0,0 +1,139 @@ +// 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 com.cloud.usage.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import org.apache.cloudstack.backup.Backup; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.usage.UsageBackupVO; +import com.cloud.utils.DateUtil; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.QueryBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.db.TransactionStatus; +import com.cloud.vm.VirtualMachine; + +@Component +public class UsageBackupDaoImpl extends GenericDaoBase implements UsageBackupDao { + public static final Logger LOGGER = Logger.getLogger(UsageBackupDaoImpl.class); + protected static final String GET_USAGE_RECORDS_BY_ACCOUNT = "SELECT id, zone_id, account_id, domain_id, vm_id, backup_offering_id, size, protected_size, created, removed FROM cloud_usage.usage_backup WHERE " + + " account_id = ? AND ((removed IS NULL AND created <= ?) OR (created BETWEEN ? AND ?) OR (removed BETWEEN ? AND ?) " + + " OR ((created <= ?) AND (removed >= ?)))"; + + @Override + public void updateMetrics(final VirtualMachine vm, Backup.Metric metric) { + boolean result = Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback() { + @Override + public Boolean doInTransaction(final TransactionStatus status) { + final QueryBuilder qb = QueryBuilder.create(UsageBackupVO.class); + qb.and(qb.entity().getVmId(), SearchCriteria.Op.EQ, vm.getId()); + final UsageBackupVO entry = findOneBy(qb.create()); + if (entry == null) { + return false; + } + entry.setSize(metric.getBackupSize()); + entry.setProtectedSize(metric.getDataSize()); + return update(entry.getId(), entry); + } + }); + if (!result) { + LOGGER.trace("Failed to update backup metrics for VM ID: " + vm.getId()); + } + } + + @Override + public void removeUsage(Long accountId, Long zoneId, Long vmId) { + boolean result = Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback() { + @Override + public Boolean doInTransaction(final TransactionStatus status) { + final QueryBuilder qb = QueryBuilder.create(UsageBackupVO.class); + qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); + qb.and(qb.entity().getZoneId(), SearchCriteria.Op.EQ, zoneId); + qb.and(qb.entity().getVmId(), SearchCriteria.Op.EQ, vmId); + final UsageBackupVO entry = findOneBy(qb.create()); + return remove(qb.create()) > 0; + } + }); + if (!result) { + LOGGER.warn("Failed to remove usage entry for backup of VM ID: " + vmId); + } + } + + @Override + public List getUsageRecords(Long accountId, Date startDate, Date endDate) { + List usageRecords = new ArrayList(); + TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.USAGE_DB); + PreparedStatement pstmt; + try { + int i = 1; + pstmt = txn.prepareAutoCloseStatement(GET_USAGE_RECORDS_BY_ACCOUNT); + pstmt.setLong(i++, accountId); + + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), endDate)); + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), startDate)); + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), endDate)); + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), startDate)); + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), endDate)); + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), startDate)); + pstmt.setString(i++, DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), endDate)); + + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + //id, zone_id, account_id, domain_id, vm_id, disk_offering_id, size, created, processed + Long id = Long.valueOf(rs.getLong(1)); + Long zoneId = Long.valueOf(rs.getLong(2)); + Long acctId = Long.valueOf(rs.getLong(3)); + Long domId = Long.valueOf(rs.getLong(4)); + Long vmId = Long.valueOf(rs.getLong(5)); + Long backupOfferingId = Long.valueOf(rs.getLong(6)); + Long size = Long.valueOf(rs.getLong(7)); + Long pSize = Long.valueOf(rs.getLong(8)); + Date createdDate = null; + Date removedDate = null; + String createdTS = rs.getString(9); + String removedTS = rs.getString(10); + + if (createdTS != null) { + createdDate = DateUtil.parseDateString(s_gmtTimeZone, createdTS); + } + if (removedTS != null) { + removedDate = DateUtil.parseDateString(s_gmtTimeZone, removedTS); + } + usageRecords.add(new UsageBackupVO(id, zoneId, acctId, domId, vmId, backupOfferingId, size, pSize, createdDate, removedDate)); + } + } catch (Exception e) { + txn.rollback(); + LOGGER.warn("Error getting VM backup usage records", e); + } finally { + txn.close(); + } + + return usageRecords; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java index b0ebf2406f5..5c81e550e48 100644 --- a/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/VMInstanceVO.java @@ -16,14 +16,14 @@ // under the License. package com.cloud.vm; -import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.utils.db.Encrypt; -import com.cloud.utils.db.GenericDao; -import com.cloud.utils.db.StateMachine; -import com.cloud.utils.fsm.FiniteStateObject; -import com.cloud.vm.VirtualMachine.State; -import org.apache.commons.codec.binary.Base64; -import org.apache.log4j.Logger; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; import javax.persistence.Column; import javax.persistence.DiscriminatorColumn; @@ -39,11 +39,19 @@ import javax.persistence.TableGenerator; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Date; -import java.util.Map; -import java.util.UUID; + +import org.apache.cloudstack.backup.Backup; +import org.apache.commons.codec.binary.Base64; +import org.apache.log4j.Logger; + +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.utils.db.Encrypt; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.StateMachine; +import com.cloud.utils.fsm.FiniteStateObject; +import com.cloud.vm.VirtualMachine.State; +import com.google.common.base.Strings; +import com.google.gson.Gson; @Entity @Table(name = "vm_instance") @@ -186,6 +194,15 @@ public class VMInstanceVO implements VirtualMachine, FiniteStateObject getBackupVolumeList() { + if (Strings.isNullOrEmpty(this.backupVolumes)) { + return Collections.emptyList(); + } + return Arrays.asList(new Gson().fromJson(this.backupVolumes, Backup.VolumeInfo[].class)); + } + + public void setBackupVolumes(String backupVolumes) { + this.backupVolumes = backupVolumes; + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java index 316c2dd53d7..f06fb0fea83 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java @@ -86,4 +86,6 @@ public interface NicDao extends GenericDao { Long getPeerRouterId(String publicMacAddress, long routerId); List listByVmIdAndKeyword(long instanceId, String keyword); + + NicVO findByInstanceIdAndMacAddress(long instanceId, String macAddress); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java index 6314ca0391b..18630e85c57 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java @@ -364,4 +364,12 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { sc.setParameters("address", "%" + keyword + "%"); return listBy(sc); } + + @Override + public NicVO findByInstanceIdAndMacAddress(long instanceId, String macAddress) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("instance", instanceId); + sc.setParameters("macAddress", macAddress); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index fad97478dc5..2052fd287c8 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -86,6 +86,8 @@ public interface VMInstanceDao extends GenericDao, StateDao< VMInstanceVO findVMByInstanceName(String name); + VMInstanceVO findVMByInstanceNameIncludingRemoved(String name); + VMInstanceVO findVMByHostName(String hostName); void updateProxyId(long id, Long proxyId, Date time); @@ -114,6 +116,8 @@ public interface VMInstanceDao extends GenericDao, StateDao< List listVmsMigratingFromHost(Long hostId); + List listByZoneWithBackups(Long zoneId, Long backupOfferingId); + public Long countActiveByHostId(long hostId); Pair, Map> listClusterIdsInZoneByVmCount(long zoneId, long accountId); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 3af74ab00b4..1d7d4440566 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -94,6 +94,7 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem protected SearchBuilder HostAndStateSearch; protected SearchBuilder StartingWithNoHostSearch; protected SearchBuilder NotMigratingSearch; + protected SearchBuilder BackupSearch; protected SearchBuilder LastHostAndStatesSearch; @Inject @@ -288,6 +289,12 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem NotMigratingSearch.and("state", NotMigratingSearch.entity().getState(), Op.NEQ); NotMigratingSearch.done(); + BackupSearch = createSearchBuilder(); + BackupSearch.and("zone_id", BackupSearch.entity().getDataCenterId(), Op.EQ); + BackupSearch.and("backup_offering_not_null", BackupSearch.entity().getBackupOfferingId(), Op.NNULL); + BackupSearch.and("backup_offering_id", BackupSearch.entity().getBackupOfferingId(), Op.EQ); + BackupSearch.done(); + LastHostAndStatesSearch = createSearchBuilder(); LastHostAndStatesSearch.and("lastHost", LastHostAndStatesSearch.entity().getLastHostId(), Op.EQ); LastHostAndStatesSearch.and("states", LastHostAndStatesSearch.entity().getState(), Op.IN); @@ -456,6 +463,13 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem return findOneBy(sc); } + @Override + public VMInstanceVO findVMByInstanceNameIncludingRemoved(String name) { + SearchCriteria sc = InstanceNameSearch.create(); + sc.setParameters("instanceName", name); + return findOneIncludingRemovedBy(sc); + } + @Override public VMInstanceVO findVMByHostName(String hostName) { SearchCriteria sc = HostNameSearch.create(); @@ -591,6 +605,16 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem return listBy(sc); } + @Override + public List listByZoneWithBackups(Long zoneId, Long backupOfferingId) { + SearchCriteria sc = BackupSearch.create(); + sc.setParameters("zone_id", zoneId); + if (backupOfferingId != null) { + sc.setParameters("backup_offering_id", backupOfferingId); + } + return listBy(sc); + } + @Override public Long countActiveByHostId(long hostId) { SearchCriteria sc = CountActiveByHost.create(); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java new file mode 100644 index 00000000000..c5d8790321e --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java @@ -0,0 +1,126 @@ +// 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.backup; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +@Entity +@Table(name = "backup_offering") +public class BackupOfferingVO implements BackupOffering { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "description") + private String description; + + @Column(name = "external_id") + private String externalId; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "user_driven_backup") + private boolean userDrivenBackupAllowed; + + @Column(name = "provider") + private String provider; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + public BackupOfferingVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public BackupOfferingVO(final long zoneId, final String externalId, final String provider, final String name, final String description, final boolean userDrivenBackupAllowed) { + this(); + this.name = name; + this.description = description; + this.zoneId = zoneId; + this.provider = provider; + this.externalId = externalId; + this.userDrivenBackupAllowed = userDrivenBackupAllowed; + this.created = new Date(); + } + + public String getUuid() { + return uuid; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getExternalId() { + return externalId; + } + + @Override + public long getZoneId() { + return zoneId; + } + + @Override + public boolean isUserDrivenBackupAllowed() { + return userDrivenBackupAllowed; + } + + public void setUserDrivenBackupAllowed(boolean userDrivenBackupAllowed) { + this.userDrivenBackupAllowed = userDrivenBackupAllowed; + } + + @Override + public String getProvider() { + return provider; + } + + public String getDescription() { + return description; + } + + public Date getCreated() { + return created; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java new file mode 100644 index 00000000000..ba31dc59d39 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java @@ -0,0 +1,124 @@ +// 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.backup; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.utils.DateUtil; + +@Entity +@Table(name = "backup_schedule") +public class BackupScheduleVO implements BackupSchedule { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "vm_id") + private Long vmId; + + @Column(name = "schedule_type") + private Short scheduleType; + + @Column(name = "schedule") + String schedule; + + @Column(name = "timezone") + String timezone; + + @Column(name = "scheduled_timestamp") + @Temporal(value = TemporalType.TIMESTAMP) + Date scheduledTimestamp; + + @Column(name = "async_job_id") + Long asyncJobId; + + public BackupScheduleVO() { + } + + public BackupScheduleVO(Long vmId, DateUtil.IntervalType scheduleType, String schedule, String timezone, Date scheduledTimestamp) { + this.vmId = vmId; + this.scheduleType = (short) scheduleType.ordinal(); + this.schedule = schedule; + this.timezone = timezone; + this.scheduledTimestamp = scheduledTimestamp; + } + + @Override + public long getId() { + return id; + } + + public Long getVmId() { + return vmId; + } + + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + @Override + public DateUtil.IntervalType getScheduleType() { + return scheduleType == null ? null : DateUtil.getIntervalType(scheduleType); + } + + public void setScheduleType(Short intervalType) { + this.scheduleType = intervalType; + } + + public String getSchedule() { + return schedule; + } + + public void setSchedule(String schedule) { + this.schedule = schedule; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public Date getScheduledTimestamp() { + return scheduledTimestamp; + } + + public void setScheduledTimestamp(Date scheduledTimestamp) { + this.scheduledTimestamp = scheduledTimestamp; + } + + public Long getAsyncJobId() { + return asyncJobId; + } + + public void setAsyncJobId(Long asyncJobId) { + this.asyncJobId = asyncJobId; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java new file mode 100644 index 00000000000..e56f55cbf59 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java @@ -0,0 +1,190 @@ +//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 +//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.backup; + +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "backups") +public class BackupVO implements Backup { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "external_id") + private String externalId; + + @Column(name = "type") + private String backupType; + + @Column(name = "date") + private String date; + + @Column(name = "size") + private Long size; + + @Column(name = "protected_size") + private Long protectedSize; + + @Enumerated(value = EnumType.STRING) + @Column(name = "status") + private Backup.Status status; + + @Column(name = "backup_offering_id") + private long backupOfferingId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "zone_id") + private long zoneId; + + public BackupVO() { + this.uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public long getVmId() { + return vmId; + } + + public void setVmId(long vmId) { + this.vmId = vmId; + } + + @Override + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getType() { + return backupType; + } + + public void setType(String type) { + this.backupType = type; + } + + @Override + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + @Override + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + @Override + public Long getProtectedSize() { + return protectedSize; + } + + public void setProtectedSize(Long protectedSize) { + this.protectedSize = protectedSize; + } + + @Override + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public long getBackupOfferingId() { + return backupOfferingId; + } + + public void setBackupOfferingId(long backupOfferingId) { + this.backupOfferingId = backupOfferingId; + } + + @Override + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + @Override + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + @Override + public Class getEntityType() { + return Backup.class; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java new file mode 100644 index 00000000000..5d2f5ac64d6 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDao.java @@ -0,0 +1,40 @@ +// 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.backup.dao; + +import java.util.List; + +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupVO; + +import com.cloud.utils.db.GenericDao; + +public interface BackupDao extends GenericDao { + + Backup findByVmId(Long vmId); + Backup findByVmIdIncludingRemoved(Long vmId); + + List listByVmId(Long zoneId, Long vmId); + List listByAccountId(Long accountId); + List listByOfferingId(Long offeringId); + List syncBackups(Long zoneId, Long vmId, List externalBackups); + BackupVO getBackupVO(Backup backup); + + BackupResponse newBackupResponse(Backup backup); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java new file mode 100644 index 00000000000..fefbb68ae77 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupDaoImpl.java @@ -0,0 +1,172 @@ +// 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.backup.dao; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupVO; + +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.user.AccountVO; +import com.cloud.user.dao.AccountDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; +import com.google.gson.Gson; + +public class BackupDaoImpl extends GenericDaoBase implements BackupDao { + + @Inject + AccountDao accountDao; + + @Inject + DomainDao domainDao; + + @Inject + DataCenterDao dataCenterDao; + + @Inject + VMInstanceDao vmInstanceDao; + + @Inject + BackupOfferingDao backupOfferingDao; + + private SearchBuilder backupSearch; + + public BackupDaoImpl() { + } + + @PostConstruct + protected void init() { + backupSearch = createSearchBuilder(); + backupSearch.and("vm_id", backupSearch.entity().getVmId(), SearchCriteria.Op.EQ); + backupSearch.and("external_id", backupSearch.entity().getExternalId(), SearchCriteria.Op.EQ); + backupSearch.done(); + } + + @Override + public List listByAccountId(Long accountId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("account_id", accountId); + return new ArrayList<>(listBy(sc)); + } + + @Override + public Backup findByVmId(Long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("vm_id", vmId); + return findOneBy(sc); + } + + @Override + public Backup findByVmIdIncludingRemoved(Long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("vm_id", vmId); + return findOneIncludingRemovedBy(sc); + } + + @Override + public List listByVmId(Long zoneId, Long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("vm_id", vmId); + if (zoneId != null) { + sc.setParameters("zone_id", zoneId); + } + return new ArrayList<>(listBy(sc)); + } + + @Override + public List listByOfferingId(Long offeringId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("offering_id", offeringId); + return new ArrayList<>(listBy(sc)); + } + + private Backup findByExternalId(Long zoneId, String externalId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("external_id", externalId); + sc.setParameters("zone_id", zoneId); + return findOneBy(sc); + } + + public BackupVO getBackupVO(Backup backup) { + BackupVO backupVO = new BackupVO(); + backupVO.setExternalId(backup.getExternalId()); + backupVO.setVmId(backup.getVmId()); + return backupVO; + } + + public void removeExistingBackups(Long zoneId, Long vmId) { + SearchCriteria sc = backupSearch.create(); + sc.setParameters("vm_id", vmId); + sc.setParameters("zone_id", zoneId); + expunge(sc); + } + + @Override + public List syncBackups(Long zoneId, Long vmId, List externalBackups) { + for (Backup backup : externalBackups) { + BackupVO backupVO = getBackupVO(backup); + persist(backupVO); + } + return listByVmId(zoneId, vmId); + } + + @Override + public BackupResponse newBackupResponse(Backup backup) { + VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); + AccountVO account = accountDao.findByIdIncludingRemoved(vm.getAccountId()); + DomainVO domain = domainDao.findByIdIncludingRemoved(vm.getDomainId()); + DataCenterVO zone = dataCenterDao.findByIdIncludingRemoved(vm.getDataCenterId()); + BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId()); + + BackupResponse response = new BackupResponse(); + response.setId(backup.getUuid()); + response.setVmId(vm.getUuid()); + response.setVmName(vm.getHostName()); + response.setExternalId(backup.getExternalId()); + response.setType(backup.getType()); + response.setDate(backup.getDate()); + response.setSize(backup.getSize()); + response.setProtectedSize(backup.getProtectedSize()); + response.setStatus(backup.getStatus()); + response.setVolumes(new Gson().toJson(vm.getBackupVolumeList().toArray(), Backup.VolumeInfo[].class)); + response.setBackupOfferingId(offering.getUuid()); + response.setBackupOffering(offering.getName()); + response.setAccountId(account.getUuid()); + response.setAccount(account.getAccountName()); + response.setDomainId(domain.getUuid()); + response.setDomain(domain.getName()); + response.setZoneId(zone.getUuid()); + response.setZone(zone.getName()); + response.setObjectName("backup"); + return response; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDao.java new file mode 100644 index 00000000000..d001de8b6c6 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDao.java @@ -0,0 +1,30 @@ +// 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.backup.dao; + +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupOfferingVO; + +import com.cloud.utils.db.GenericDao; + +public interface BackupOfferingDao extends GenericDao { + BackupOfferingResponse newBackupOfferingResponse(BackupOffering policy); + BackupOffering findByExternalId(String externalId, Long zoneId); + BackupOffering findByName(String name, Long zoneId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java new file mode 100644 index 00000000000..0568a0185bb --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java @@ -0,0 +1,88 @@ +// 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.backup.dao; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupOfferingVO; + +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +public class BackupOfferingDaoImpl extends GenericDaoBase implements BackupOfferingDao { + + @Inject + DataCenterDao dataCenterDao; + + private SearchBuilder backupPoliciesSearch; + + public BackupOfferingDaoImpl() { + } + + @PostConstruct + protected void init() { + backupPoliciesSearch = createSearchBuilder(); + backupPoliciesSearch.and("name", backupPoliciesSearch.entity().getName(), SearchCriteria.Op.EQ); + backupPoliciesSearch.and("zone_id", backupPoliciesSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + backupPoliciesSearch.and("external_id", backupPoliciesSearch.entity().getExternalId(), SearchCriteria.Op.EQ); + backupPoliciesSearch.done(); + } + + @Override + public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering) { + DataCenterVO zone = dataCenterDao.findById(offering.getZoneId()); + + BackupOfferingResponse response = new BackupOfferingResponse(); + response.setId(offering.getUuid()); + response.setName(offering.getName()); + response.setDescription(offering.getDescription()); + response.setExternalId(offering.getExternalId()); + response.setUserDrivenBackups(offering.isUserDrivenBackupAllowed()); + if (zone != null) { + response.setZoneId(zone.getUuid()); + response.setZoneName(zone.getName()); + } + response.setCreated(offering.getCreated()); + response.setObjectName("backupoffering"); + return response; + } + + @Override + public BackupOffering findByExternalId(String externalId, Long zoneId) { + SearchCriteria sc = backupPoliciesSearch.create(); + sc.setParameters("external_id", externalId); + if (zoneId != null) { + sc.setParameters("zone_id", zoneId); + } + return findOneBy(sc); + } + + @Override + public BackupOffering findByName(String name, Long zoneId) { + SearchCriteria sc = backupPoliciesSearch.create(); + sc.setParameters("name", name); + sc.setParameters("zone_id", zoneId); + return findOneBy(sc); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java new file mode 100644 index 00000000000..516b0112c98 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDao.java @@ -0,0 +1,35 @@ +// 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.backup.dao; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.backup.BackupSchedule; +import org.apache.cloudstack.backup.BackupScheduleVO; + +import com.cloud.utils.db.GenericDao; + +public interface BackupScheduleDao extends GenericDao { + BackupScheduleVO findByVM(Long vmId); + + List getSchedulesToExecute(Date currentTimestamp); + + BackupScheduleResponse newBackupScheduleResponse(BackupSchedule schedule); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java new file mode 100644 index 00000000000..7a58679e7e5 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java @@ -0,0 +1,86 @@ +// 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.backup.dao; + +import java.util.Date; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.apache.cloudstack.api.response.BackupScheduleResponse; +import org.apache.cloudstack.backup.BackupSchedule; +import org.apache.cloudstack.backup.BackupScheduleVO; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; + +public class BackupScheduleDaoImpl extends GenericDaoBase implements BackupScheduleDao { + + @Inject + VMInstanceDao vmInstanceDao; + + private SearchBuilder backupScheduleSearch; + private SearchBuilder executableSchedulesSearch; + + public BackupScheduleDaoImpl() { + } + + @PostConstruct + protected void init() { + backupScheduleSearch = createSearchBuilder(); + backupScheduleSearch.and("vm_id", backupScheduleSearch.entity().getVmId(), SearchCriteria.Op.EQ); + backupScheduleSearch.and("async_job_id", backupScheduleSearch.entity().getAsyncJobId(), SearchCriteria.Op.EQ); + backupScheduleSearch.done(); + + executableSchedulesSearch = createSearchBuilder(); + executableSchedulesSearch.and("scheduledTimestamp", executableSchedulesSearch.entity().getScheduledTimestamp(), SearchCriteria.Op.LT); + executableSchedulesSearch.and("asyncJobId", executableSchedulesSearch.entity().getAsyncJobId(), SearchCriteria.Op.NULL); + executableSchedulesSearch.done(); + } + + @Override + public BackupScheduleVO findByVM(Long vmId) { + SearchCriteria sc = backupScheduleSearch.create(); + sc.setParameters("vm_id", vmId); + return findOneBy(sc); + } + + @Override + public List getSchedulesToExecute(Date currentTimestamp) { + SearchCriteria sc = executableSchedulesSearch.create(); + sc.setParameters("scheduledTimestamp", currentTimestamp); + return listBy(sc); + } + + @Override + public BackupScheduleResponse newBackupScheduleResponse(BackupSchedule schedule) { + VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(schedule.getVmId()); + BackupScheduleResponse response = new BackupScheduleResponse(); + response.setVmId(vm.getUuid()); + response.setVmName(vm.getHostName()); + response.setIntervalType(schedule.getScheduleType()); + response.setSchedule(schedule.getSchedule()); + response.setTimezone(schedule.getTimezone()); + response.setObjectName("backupschedule"); + return response; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java b/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java index cdb80febdcf..4ab5f427591 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/engine/cloud/entity/api/db/VMEntityVO.java @@ -16,13 +16,12 @@ // under the License. package org.apache.cloudstack.engine.cloud.entity.api.db; -import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.utils.db.Encrypt; -import com.cloud.utils.db.GenericDao; -import com.cloud.utils.db.StateMachine; -import com.cloud.utils.fsm.FiniteStateObject; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VirtualMachine.State; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; import javax.persistence.Column; import javax.persistence.DiscriminatorColumn; @@ -38,11 +37,17 @@ import javax.persistence.TableGenerator; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; -import java.security.SecureRandom; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.UUID; + +import org.apache.cloudstack.backup.Backup; + +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.utils.db.Encrypt; +import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.StateMachine; +import com.cloud.utils.fsm.FiniteStateObject; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.State; +import com.google.gson.Gson; @Entity @Table(name = "vm_instance") @@ -175,6 +180,15 @@ public class VMEntityVO implements VirtualMachine, FiniteStateObject getBackupVolumeList() { + return Arrays.asList(new Gson().fromJson(this.backupVolumes, Backup.VolumeInfo[].class)); + } } diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 34c356dab34..7faf85cef4b 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -218,6 +218,7 @@ + @@ -285,6 +286,9 @@ + + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41300to41400.sql b/engine/schema/src/main/resources/META-INF/db/schema-41300to41400.sql index ad257bc74e7..db7482b5eed 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41300to41400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41300to41400.sql @@ -28,6 +28,256 @@ UPDATE `cloud`.`vm_template` SET guest_os_id=99 WHERE id=1; -- #3659 Fix typo: the past tense of shutdown is shutdown, not shutdowned UPDATE `cloud`.`vm_instance` SET state='Shutdown' WHERE state='Shutdowned'; +-- Backup and Recovery + +CREATE TABLE IF NOT EXISTS `cloud`.`backup_offering` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL UNIQUE, + `name` varchar(255) NOT NULL COMMENT 'backup offering name', + `description` varchar(255) NOT NULL COMMENT 'backup offering description', + `external_id` varchar(255) DEFAULT NULL COMMENT 'external ID on provider side', + `user_driven_backup` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'whether user can do adhoc backups and backup schedules allowed, default false', + `zone_id` bigint(20) unsigned NOT NULL COMMENT 'zone id', + `provider` varchar(255) NOT NULL COMMENT 'backup provider', + `created` datetime DEFAULT NULL, + `removed` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_backup_offering__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE `cloud`.`vm_instance` ADD COLUMN `backup_offering_id` bigint unsigned DEFAULT NULL COMMENT 'ID of backup offering'; +ALTER TABLE `cloud`.`vm_instance` ADD COLUMN `backup_external_id` varchar(255) DEFAULT NULL COMMENT 'ID of external backup job or container if any'; +ALTER TABLE `cloud`.`vm_instance` ADD COLUMN `backup_volumes` text DEFAULT NULL COMMENT 'details of backedup volumes'; + +CREATE TABLE IF NOT EXISTS `cloud`.`backups` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL UNIQUE, + `vm_id` bigint(20) unsigned NOT NULL, + `external_id` varchar(255) DEFAULT NULL COMMENT 'external ID', + `type` varchar(255) NOT NULL COMMENT 'backup type', + `date` varchar(255) NOT NULL COMMENT 'backup date', + `size` bigint(20) DEFAULT 0 COMMENT 'size of the backup', + `protected_size` bigint(20) DEFAULT 0, + `status` varchar(32) DEFAULT NULL, + `backup_offering_id` bigint(20) unsigned NOT NULL, + `account_id` bigint(20) unsigned NOT NULL, + `domain_id` bigint(20) unsigned NOT NULL, + `zone_id` bigint(20) unsigned NOT NULL, + `removed` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_backup__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_backup__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `cloud`.`backup_schedule` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `vm_id` bigint(20) unsigned NOT NULL UNIQUE, + `schedule_type` int(4) DEFAULT NULL COMMENT 'backup schedulet type e.g. hourly, daily, etc.', + `schedule` varchar(100) DEFAULT NULL COMMENT 'schedule time of execution', + `timezone` varchar(100) DEFAULT NULL COMMENT 'the timezone in which the schedule time is specified', + `scheduled_timestamp` datetime DEFAULT NULL COMMENT 'Time at which the backup was scheduled for execution', + `async_job_id` bigint(20) unsigned DEFAULT NULL COMMENT 'If this schedule is being executed, it is the id of the create aysnc_job. Before that it is null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_backup_schedule__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `cloud_usage`.`usage_backup` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `zone_id` bigint(20) unsigned NOT NULL, + `account_id` bigint(20) unsigned NOT NULL, + `domain_id` bigint(20) unsigned NOT NULL, + `vm_id` bigint(20) unsigned NOT NULL, + `backup_offering_id` bigint(20) unsigned NOT NULL, + `size` bigint(20) DEFAULT 0, + `protected_size` bigint(20) DEFAULT 0, + `created` datetime NOT NULL, + `removed` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `i_usage_backup` (`zone_id`,`account_id`,`vm_id`,`created`) +) ENGINE=InnoDB CHARSET=utf8; + +DROP VIEW IF EXISTS `cloud`.`user_vm_view`; +CREATE +VIEW `user_vm_view` AS + SELECT + `vm_instance`.`id` AS `id`, + `vm_instance`.`name` AS `name`, + `user_vm`.`display_name` AS `display_name`, + `user_vm`.`user_data` AS `user_data`, + `account`.`id` AS `account_id`, + `account`.`uuid` AS `account_uuid`, + `account`.`account_name` AS `account_name`, + `account`.`type` AS `account_type`, + `domain`.`id` AS `domain_id`, + `domain`.`uuid` AS `domain_uuid`, + `domain`.`name` AS `domain_name`, + `domain`.`path` AS `domain_path`, + `projects`.`id` AS `project_id`, + `projects`.`uuid` AS `project_uuid`, + `projects`.`name` AS `project_name`, + `instance_group`.`id` AS `instance_group_id`, + `instance_group`.`uuid` AS `instance_group_uuid`, + `instance_group`.`name` AS `instance_group_name`, + `vm_instance`.`uuid` AS `uuid`, + `vm_instance`.`user_id` AS `user_id`, + `vm_instance`.`last_host_id` AS `last_host_id`, + `vm_instance`.`vm_type` AS `type`, + `vm_instance`.`limit_cpu_use` AS `limit_cpu_use`, + `vm_instance`.`created` AS `created`, + `vm_instance`.`state` AS `state`, + `vm_instance`.`removed` AS `removed`, + `vm_instance`.`ha_enabled` AS `ha_enabled`, + `vm_instance`.`hypervisor_type` AS `hypervisor_type`, + `vm_instance`.`instance_name` AS `instance_name`, + `vm_instance`.`guest_os_id` AS `guest_os_id`, + `vm_instance`.`display_vm` AS `display_vm`, + `guest_os`.`uuid` AS `guest_os_uuid`, + `vm_instance`.`pod_id` AS `pod_id`, + `host_pod_ref`.`uuid` AS `pod_uuid`, + `vm_instance`.`private_ip_address` AS `private_ip_address`, + `vm_instance`.`private_mac_address` AS `private_mac_address`, + `vm_instance`.`vm_type` AS `vm_type`, + `data_center`.`id` AS `data_center_id`, + `data_center`.`uuid` AS `data_center_uuid`, + `data_center`.`name` AS `data_center_name`, + `data_center`.`is_security_group_enabled` AS `security_group_enabled`, + `data_center`.`networktype` AS `data_center_type`, + `host`.`id` AS `host_id`, + `host`.`uuid` AS `host_uuid`, + `host`.`name` AS `host_name`, + `vm_template`.`id` AS `template_id`, + `vm_template`.`uuid` AS `template_uuid`, + `vm_template`.`name` AS `template_name`, + `vm_template`.`display_text` AS `template_display_text`, + `vm_template`.`enable_password` AS `password_enabled`, + `iso`.`id` AS `iso_id`, + `iso`.`uuid` AS `iso_uuid`, + `iso`.`name` AS `iso_name`, + `iso`.`display_text` AS `iso_display_text`, + `service_offering`.`id` AS `service_offering_id`, + `svc_disk_offering`.`uuid` AS `service_offering_uuid`, + `disk_offering`.`uuid` AS `disk_offering_uuid`, + `disk_offering`.`id` AS `disk_offering_id`, + (CASE + WHEN ISNULL(`service_offering`.`cpu`) THEN `custom_cpu`.`value` + ELSE `service_offering`.`cpu` + END) AS `cpu`, + (CASE + WHEN ISNULL(`service_offering`.`speed`) THEN `custom_speed`.`value` + ELSE `service_offering`.`speed` + END) AS `speed`, + (CASE + WHEN ISNULL(`service_offering`.`ram_size`) THEN `custom_ram_size`.`value` + ELSE `service_offering`.`ram_size` + END) AS `ram_size`, + `backup_offering`.`uuid` AS `backup_offering_uuid`, + `backup_offering`.`id` AS `backup_offering_id`, + `svc_disk_offering`.`name` AS `service_offering_name`, + `disk_offering`.`name` AS `disk_offering_name`, + `backup_offering`.`name` AS `backup_offering_name`, + `storage_pool`.`id` AS `pool_id`, + `storage_pool`.`uuid` AS `pool_uuid`, + `storage_pool`.`pool_type` AS `pool_type`, + `volumes`.`id` AS `volume_id`, + `volumes`.`uuid` AS `volume_uuid`, + `volumes`.`device_id` AS `volume_device_id`, + `volumes`.`volume_type` AS `volume_type`, + `security_group`.`id` AS `security_group_id`, + `security_group`.`uuid` AS `security_group_uuid`, + `security_group`.`name` AS `security_group_name`, + `security_group`.`description` AS `security_group_description`, + `nics`.`id` AS `nic_id`, + `nics`.`uuid` AS `nic_uuid`, + `nics`.`network_id` AS `network_id`, + `nics`.`ip4_address` AS `ip_address`, + `nics`.`ip6_address` AS `ip6_address`, + `nics`.`ip6_gateway` AS `ip6_gateway`, + `nics`.`ip6_cidr` AS `ip6_cidr`, + `nics`.`default_nic` AS `is_default_nic`, + `nics`.`gateway` AS `gateway`, + `nics`.`netmask` AS `netmask`, + `nics`.`mac_address` AS `mac_address`, + `nics`.`broadcast_uri` AS `broadcast_uri`, + `nics`.`isolation_uri` AS `isolation_uri`, + `vpc`.`id` AS `vpc_id`, + `vpc`.`uuid` AS `vpc_uuid`, + `networks`.`uuid` AS `network_uuid`, + `networks`.`name` AS `network_name`, + `networks`.`traffic_type` AS `traffic_type`, + `networks`.`guest_type` AS `guest_type`, + `user_ip_address`.`id` AS `public_ip_id`, + `user_ip_address`.`uuid` AS `public_ip_uuid`, + `user_ip_address`.`public_ip_address` AS `public_ip_address`, + `ssh_keypairs`.`keypair_name` AS `keypair_name`, + `resource_tags`.`id` AS `tag_id`, + `resource_tags`.`uuid` AS `tag_uuid`, + `resource_tags`.`key` AS `tag_key`, + `resource_tags`.`value` AS `tag_value`, + `resource_tags`.`domain_id` AS `tag_domain_id`, + `domain`.`uuid` AS `tag_domain_uuid`, + `domain`.`name` AS `tag_domain_name`, + `resource_tags`.`account_id` AS `tag_account_id`, + `account`.`account_name` AS `tag_account_name`, + `resource_tags`.`resource_id` AS `tag_resource_id`, + `resource_tags`.`resource_uuid` AS `tag_resource_uuid`, + `resource_tags`.`resource_type` AS `tag_resource_type`, + `resource_tags`.`customer` AS `tag_customer`, + `async_job`.`id` AS `job_id`, + `async_job`.`uuid` AS `job_uuid`, + `async_job`.`job_status` AS `job_status`, + `async_job`.`account_id` AS `job_account_id`, + `affinity_group`.`id` AS `affinity_group_id`, + `affinity_group`.`uuid` AS `affinity_group_uuid`, + `affinity_group`.`name` AS `affinity_group_name`, + `affinity_group`.`description` AS `affinity_group_description`, + `vm_instance`.`dynamically_scalable` AS `dynamically_scalable` + FROM + (((((((((((((((((((((((((((((((((`user_vm` + JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) + AND ISNULL(`vm_instance`.`removed`)))) + JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) + JOIN `domain` ON ((`vm_instance`.`domain_id` = `domain`.`id`))) + LEFT JOIN `guest_os` ON ((`vm_instance`.`guest_os_id` = `guest_os`.`id`))) + LEFT JOIN `host_pod_ref` ON ((`vm_instance`.`pod_id` = `host_pod_ref`.`id`))) + LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`))) + LEFT JOIN `instance_group_vm_map` ON ((`vm_instance`.`id` = `instance_group_vm_map`.`instance_id`))) + LEFT JOIN `instance_group` ON ((`instance_group_vm_map`.`group_id` = `instance_group`.`id`))) + LEFT JOIN `data_center` ON ((`vm_instance`.`data_center_id` = `data_center`.`id`))) + LEFT JOIN `host` ON ((`vm_instance`.`host_id` = `host`.`id`))) + LEFT JOIN `vm_template` ON ((`vm_instance`.`vm_template_id` = `vm_template`.`id`))) + LEFT JOIN `vm_template` `iso` ON ((`iso`.`id` = `user_vm`.`iso_id`))) + LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) + LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`vm_instance`.`service_offering_id` = `svc_disk_offering`.`id`))) + LEFT JOIN `disk_offering` ON ((`vm_instance`.`disk_offering_id` = `disk_offering`.`id`))) + LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`))) + LEFT JOIN `volumes` ON ((`vm_instance`.`id` = `volumes`.`instance_id`))) + LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`))) + LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`))) + LEFT JOIN `security_group` ON ((`security_group_vm_map`.`security_group_id` = `security_group`.`id`))) + LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) + AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) + LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) + AND ISNULL(`vpc`.`removed`)))) + LEFT JOIN `user_ip_address` ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`))) + LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) + AND (`ssh_details`.`name` = 'SSH.PublicKey')))) + LEFT JOIN `ssh_keypairs` ON (((`ssh_keypairs`.`public_key` = `ssh_details`.`value`) + AND (`ssh_keypairs`.`account_id` = `account`.`id`)))) + LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`) + AND (`resource_tags`.`resource_type` = 'UserVm')))) + LEFT JOIN `async_job` ON (((`async_job`.`instance_id` = `vm_instance`.`id`) + AND (`async_job`.`instance_type` = 'VirtualMachine') + AND (`async_job`.`job_status` = 0)))) + LEFT JOIN `affinity_group_vm_map` ON ((`vm_instance`.`id` = `affinity_group_vm_map`.`instance_id`))) + LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`))) + LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) + AND (`custom_cpu`.`name` = 'CpuNumber')))) + LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) + AND (`custom_speed`.`name` = 'CpuSpeed')))) + LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) + AND (`custom_ram_size`.`name` = 'memory')))); + -- Fix OS category for some Ubuntu and RedHat OS-es UPDATE `cloud`.`guest_os` SET `category_id`='10' WHERE `id`=277 AND display_name="Ubuntu 17.04"; UPDATE `cloud`.`guest_os` SET `category_id`='10' WHERE `id`=278 AND display_name="Ubuntu 17.10"; diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/DiskOfferingDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/DiskOfferingDaoImplTest.java new file mode 100644 index 00000000000..3dc36d6b4d3 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/dao/DiskOfferingDaoImplTest.java @@ -0,0 +1,56 @@ +// 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 com.cloud.storage.dao; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.junit.Assert; +import org.junit.Test; + +public class DiskOfferingDaoImplTest { + + private final DiskOfferingDaoImpl dao = new DiskOfferingDaoImpl(); + + @Test(expected = CloudRuntimeException.class) + public void testGetClosestDiskSizeInGBNegativeSize() { + long size = -4 * DiskOfferingDaoImpl.GB_UNIT_BYTES; + dao.getClosestDiskSizeInGB(size); + } + + @Test + public void testGetClosestDiskSizeInGBSizeGB() { + int gbUnits = 5; + long size = gbUnits * DiskOfferingDaoImpl.GB_UNIT_BYTES; + long sizeInGB = dao.getClosestDiskSizeInGB(size); + Assert.assertEquals(gbUnits, sizeInGB); + } + + @Test + public void testGetClosestDiskSizeInGBSizeGBRest() { + int gbUnits = 5; + long size = gbUnits * DiskOfferingDaoImpl.GB_UNIT_BYTES + 12345; + long sizeInGB = dao.getClosestDiskSizeInGB(size); + Assert.assertEquals(gbUnits + 1, sizeInGB); + } + + @Test + public void testGetClosestDiskSizeInGBSizeLessOneGB() { + int gbUnits = 1; + long size = gbUnits * DiskOfferingDaoImpl.GB_UNIT_BYTES - 12345; + long sizeInGB = dao.getClosestDiskSizeInGB(size); + Assert.assertEquals(gbUnits, sizeInGB); + } +} diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java index 04c0882f22c..38f9d9f64a1 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDao.java @@ -234,6 +234,8 @@ public interface GenericDao { */ void expunge(); + boolean unremove(ID id); + public K getNextInSequence(Class clazz, String name); /** diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java index 314cd7c1c6c..f34b8edd64f 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java @@ -1785,6 +1785,34 @@ public abstract class GenericDaoBase extends Compone } } + @Override + public boolean unremove(ID id) { + if (_removed == null) { + return false; + } + + final TransactionLegacy txn = TransactionLegacy.currentTxn(); + PreparedStatement pstmt = null; + try { + txn.start(); + pstmt = txn.prepareAutoCloseStatement(_removeSql.first()); + final Attribute[] attrs = _removeSql.second(); + pstmt.setObject(1, null); + for (int i = 0; i < attrs.length - 1; i++) { + prepareAttribute(i + 2, pstmt, attrs[i], id); + } + + final int result = pstmt.executeUpdate(); + txn.commit(); + if (_cache != null) { + _cache.remove(id); + } + return result > 0; + } catch (final SQLException e) { + throw new CloudRuntimeException("DB Exception on: " + pstmt, e); + } + } + @DB() protected void setField(final Object entity, final ResultSet rs, ResultSetMetaData meta, final int index) throws SQLException { Attribute attr = _allColumns.get(new Pair(meta.getTableName(index), meta.getColumnName(index))); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 769f9aec92f..3eb949fca41 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -158,6 +158,7 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { case QuotaTypes.ISO: case QuotaTypes.VOLUME: case QuotaTypes.VM_SNAPSHOT: + case QuotaTypes.BACKUP: qu = updateQuotaDiskUsage(usageRecord, aggregationRatio, usageRecord.getUsageType()); if (qu != null) { quotaListForAccount.add(qu); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java index 13788f7508e..babb4edb520 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaTypes.java @@ -16,12 +16,12 @@ // under the License. package org.apache.cloudstack.quota.constant; -import org.apache.cloudstack.usage.UsageTypes; - import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.apache.cloudstack.usage.UsageTypes; + public class QuotaTypes extends UsageTypes { public static final int CPU_CLOCK_RATE = 15; public static final int CPU_NUMBER = 16; @@ -57,6 +57,7 @@ public class QuotaTypes extends UsageTypes { quotaTypeList.put(VM_SNAPSHOT, new QuotaTypes(VM_SNAPSHOT, "VM_SNAPSHOT", "GB-Month", "VM Snapshot storage usage")); quotaTypeList.put(VOLUME_SECONDARY, new QuotaTypes(VOLUME_SECONDARY, "VOLUME_SECONDARY", "GB-Month", "Volume secondary storage usage")); quotaTypeList.put(VM_SNAPSHOT_ON_PRIMARY, new QuotaTypes(VM_SNAPSHOT_ON_PRIMARY, "VM_SNAPSHOT_ON_PRIMARY", "GB-Month", "VM Snapshot primary storage usage")); + quotaTypeList.put(BACKUP, new QuotaTypes(BACKUP, "BACKUP", "GB-Month", "Backup storage usage")); quotaTypeList.put(CPU_CLOCK_RATE, new QuotaTypes(CPU_CLOCK_RATE, "CPU_CLOCK_RATE", "Compute-Month", "Quota tariff for using 1 CPU of clock rate 100MHz")); quotaTypeList.put(CPU_NUMBER, new QuotaTypes(CPU_NUMBER, "CPU_NUMBER", "Compute-Month", "Quota tariff for running VM that has 1vCPU")); quotaTypeList.put(MEMORY, new QuotaTypes(MEMORY, "MEMORY", "Compute-Month", "Quota tariff for using 1MB of RAM")); diff --git a/plugins/backup/dummy/pom.xml b/plugins/backup/dummy/pom.xml new file mode 100644 index 00000000000..5d6d7bb5be7 --- /dev/null +++ b/plugins/backup/dummy/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + cloud-plugin-backup-dummy + Apache CloudStack Plugin - Dummy Backup and Recovery Plugin + + cloudstack-plugins + org.apache.cloudstack + 4.14.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + diff --git a/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java new file mode 100644 index 00000000000..449dfbf3437 --- /dev/null +++ b/plugins/backup/dummy/src/main/java/org/apache/cloudstack/backup/DummyBackupProvider.java @@ -0,0 +1,134 @@ +// 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.backup; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.log4j.Logger; + +import com.cloud.utils.Pair; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; + +public class DummyBackupProvider extends AdapterBase implements BackupProvider { + + private static final Logger s_logger = Logger.getLogger(DummyBackupProvider.class); + + @Inject + private BackupDao backupDao; + + @Override + public String getName() { + return "dummy"; + } + + @Override + public String getDescription() { + return "Dummy Backup Plugin"; + } + + @Override + public List listBackupOfferings(Long zoneId) { + s_logger.debug("Listing backup policies on Dummy B&R Plugin"); + BackupOffering policy1 = new BackupOfferingVO(1, "gold-policy", "dummy", "Golden Policy", "Gold description", true); + BackupOffering policy2 = new BackupOfferingVO(1, "silver-policy", "dummy", "Silver Policy", "Silver description", true); + return Arrays.asList(policy1, policy2); + } + + @Override + public boolean isValidProviderOffering(Long zoneId, String uuid) { + s_logger.debug("Checking if backup offering exists on the Dummy Backup Provider"); + return true; + } + + @Override + public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) { + s_logger.debug("Creating VM backup for VM " + vm.getInstanceName() + " from backup offering " + backupOffering.getName()); + ((VMInstanceVO) vm).setBackupExternalId("dummy-external-backup-id"); + return true; + } + + @Override + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + s_logger.debug("Restoring vm " + vm.getUuid() + "from backup " + backup.getUuid() + " on the Dummy Backup Provider"); + return true; + } + + @Override + public Pair restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid) { + s_logger.debug("Restoring volume " + volumeUuid + "from backup " + backup.getUuid() + " on the Dummy Backup Provider"); + throw new CloudRuntimeException("Dummy plugin does not support this feature"); + } + + @Override + public Map getBackupMetrics(Long zoneId, List vms) { + final Map metrics = new HashMap<>(); + final Backup.Metric metric = new Backup.Metric(1000L, 100L); + for (VirtualMachine vm : vms) { + metrics.put(vm, metric); + } + return metrics; + } + + @Override + public boolean removeVMFromBackupOffering(VirtualMachine vm) { + s_logger.debug("Removing VM ID " + vm.getUuid() + " from backup offering by the Dummy Backup Provider"); + return true; + } + + @Override + public boolean willDeleteBackupsOnOfferingRemoval() { + return true; + } + + @Override + public boolean takeBackup(VirtualMachine vm) { + s_logger.debug("Starting backup for VM ID " + vm.getUuid() + " on Dummy provider"); + + BackupVO backup = new BackupVO(); + backup.setVmId(vm.getId()); + backup.setExternalId("dummy-external-id"); + backup.setType("FULL"); + backup.setDate(new Date().toString()); + backup.setSize(1024L); + backup.setProtectedSize(1024000L); + backup.setStatus(Backup.Status.BackedUp); + backup.setBackupOfferingId(vm.getBackupOfferingId()); + backup.setAccountId(vm.getAccountId()); + backup.setDomainId(vm.getDomainId()); + backup.setZoneId(vm.getDataCenterId()); + return backupDao.persist(backup) != null; + } + + @Override + public boolean deleteBackup(Backup backup) { + return true; + } + + @Override + public void syncBackups(VirtualMachine vm, Backup.Metric metric) { + } +} diff --git a/plugins/backup/dummy/src/main/resources/META-INF/cloudstack/dummy-backup/module.properties b/plugins/backup/dummy/src/main/resources/META-INF/cloudstack/dummy-backup/module.properties new file mode 100644 index 00000000000..5969fb29054 --- /dev/null +++ b/plugins/backup/dummy/src/main/resources/META-INF/cloudstack/dummy-backup/module.properties @@ -0,0 +1,18 @@ +# 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. +name=dummy-backup +parent=backup diff --git a/plugins/backup/dummy/src/main/resources/META-INF/cloudstack/dummy-backup/spring-backup-dummy-context.xml b/plugins/backup/dummy/src/main/resources/META-INF/cloudstack/dummy-backup/spring-backup-dummy-context.xml new file mode 100644 index 00000000000..e154f9fe945 --- /dev/null +++ b/plugins/backup/dummy/src/main/resources/META-INF/cloudstack/dummy-backup/spring-backup-dummy-context.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/plugins/backup/veeam/pom.xml b/plugins/backup/veeam/pom.xml new file mode 100644 index 00000000000..e82bf1b921f --- /dev/null +++ b/plugins/backup/veeam/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + cloud-plugin-backup-veeam + Apache CloudStack Plugin - Veeam Backup and Recovery Plugin + + cloudstack-plugins + org.apache.cloudstack + 4.14.0.0-SNAPSHOT + ../../pom.xml + + + + + org.apache.cloudstack + cloud-plugin-hypervisor-vmware + ${project.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${cs.jackson.version} + + + org.apache.commons + commons-lang3 + ${cs.commons-lang3.version} + + + com.github.tomakehurst + wiremock-standalone + ${cs.wiremock.version} + test + + + + diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java new file mode 100644 index 00000000000..ee127924330 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/VeeamBackupProvider.java @@ -0,0 +1,310 @@ +// 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.backup; + +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.veeam.VeeamClient; +import org.apache.cloudstack.backup.veeam.api.Job; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.commons.collections.CollectionUtils; +import org.apache.log4j.Logger; + +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.vmware.VmwareDatacenter; +import com.cloud.hypervisor.vmware.VmwareDatacenterZoneMap; +import com.cloud.hypervisor.vmware.dao.VmwareDatacenterDao; +import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao; +import com.cloud.utils.Pair; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackNoReturn; +import com.cloud.utils.db.TransactionStatus; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; + +public class VeeamBackupProvider extends AdapterBase implements BackupProvider, Configurable { + + private static final Logger LOG = Logger.getLogger(VeeamBackupProvider.class); + public static final String BACKUP_IDENTIFIER = "-CSBKP-"; + + public ConfigKey VeeamUrl = new ConfigKey<>("Advanced", String.class, + "backup.plugin.veeam.url", "https://localhost:9398/api/", + "The Veeam backup and recovery URL.", true, ConfigKey.Scope.Zone); + + private ConfigKey VeeamUsername = new ConfigKey<>("Advanced", String.class, + "backup.plugin.veeam.username", "administrator", + "The Veeam backup and recovery username.", true, ConfigKey.Scope.Zone); + + private ConfigKey VeeamPassword = new ConfigKey<>("Secure", String.class, + "backup.plugin.veeam.password", "", + "The Veeam backup and recovery password.", true, ConfigKey.Scope.Zone); + + private ConfigKey VeeamValidateSSLSecurity = new ConfigKey<>("Advanced", Boolean.class, "backup.plugin.veeam.validate.ssl", "false", + "When set to true, this will validate the SSL certificate when connecting to https/ssl enabled Veeam API service.", true, ConfigKey.Scope.Zone); + + private ConfigKey VeeamApiRequestTimeout = new ConfigKey<>("Advanced", Integer.class, "backup.plugin.veeam.request.timeout", "300", + "The Veeam B&R API request timeout in seconds.", true, ConfigKey.Scope.Zone); + + @Inject + private VmwareDatacenterZoneMapDao vmwareDatacenterZoneMapDao; + @Inject + private VmwareDatacenterDao vmwareDatacenterDao; + @Inject + private BackupDao backupDao; + + private VeeamClient getClient(final Long zoneId) { + try { + return new VeeamClient(VeeamUrl.valueIn(zoneId), VeeamUsername.valueIn(zoneId), VeeamPassword.valueIn(zoneId), + VeeamValidateSSLSecurity.valueIn(zoneId), VeeamApiRequestTimeout.valueIn(zoneId)); + } catch (URISyntaxException e) { + throw new CloudRuntimeException("Failed to parse Veeam API URL: " + e.getMessage()); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + LOG.error("Failed to build Veeam API client due to: ", e); + } + throw new CloudRuntimeException("Failed to build Veeam API client"); + } + + public List listBackupOfferings(final Long zoneId) { + List policies = new ArrayList<>(); + for (final BackupOffering policy : getClient(zoneId).listJobs()) { + if (!policy.getName().contains(BACKUP_IDENTIFIER)) { + policies.add(policy); + } + } + return policies; + } + + @Override + public boolean isValidProviderOffering(final Long zoneId, final String uuid) { + List policies = listBackupOfferings(zoneId); + if (CollectionUtils.isEmpty(policies)) { + return false; + } + for (final BackupOffering policy : policies) { + if (policy.getExternalId().equals(uuid)) { + return true; + } + } + return false; + } + + private VmwareDatacenter findVmwareDatacenterForVM(final VirtualMachine vm) { + if (vm == null || vm.getHypervisorType() != Hypervisor.HypervisorType.VMware) { + throw new CloudRuntimeException("The Veeam backup provider is only applicable for VMware VMs"); + } + final VmwareDatacenterZoneMap zoneMap = vmwareDatacenterZoneMapDao.findByZoneId(vm.getDataCenterId()); + if (zoneMap == null) { + throw new CloudRuntimeException("Failed to find a mapped VMware datacenter for zone id:" + vm.getDataCenterId()); + } + final VmwareDatacenter vmwareDatacenter = vmwareDatacenterDao.findById(zoneMap.getVmwareDcId()); + if (vmwareDatacenter == null) { + throw new CloudRuntimeException("Failed to find a valid VMware datacenter mapped for zone id:" + vm.getDataCenterId()); + } + return vmwareDatacenter; + } + + private String getGuestBackupName(final String instanceName, final String uuid) { + return String.format("%s%s%s", instanceName, BACKUP_IDENTIFIER, uuid); + } + + @Override + public boolean assignVMToBackupOffering(final VirtualMachine vm, final BackupOffering backupOffering) { + final VeeamClient client = getClient(vm.getDataCenterId()); + final Job parentJob = client.listJob(backupOffering.getExternalId()); + final String clonedJobName = getGuestBackupName(vm.getInstanceName(), vm.getUuid()); + + if (!client.cloneVeeamJob(parentJob, clonedJobName)) { + LOG.error("Failed to clone pre-defined Veeam job (backup offering) for backup offering ID: " + backupOffering.getExternalId() + " but will check the list of jobs again if it was eventually succeeded."); + } + + for (final BackupOffering job : client.listJobs()) { + if (job.getName().equals(clonedJobName)) { + final Job clonedJob = client.listJob(job.getExternalId()); + if (clonedJob.getScheduleConfigured() && !clonedJob.getScheduleEnabled()) { + client.toggleJobSchedule(clonedJob.getId()); + } + LOG.debug("Veeam job (backup offering) for backup offering ID: " + backupOffering.getExternalId() + " found, now trying to assign the VM to the job."); + final VmwareDatacenter vmwareDC = findVmwareDatacenterForVM(vm); + if (client.addVMToVeeamJob(job.getExternalId(), vm.getInstanceName(), vmwareDC.getVcenterHost())) { + ((VMInstanceVO) vm).setBackupExternalId(job.getExternalId()); + return true; + } + } + } + return false; + } + + @Override + public boolean removeVMFromBackupOffering(final VirtualMachine vm) { + final VeeamClient client = getClient(vm.getDataCenterId()); + final VmwareDatacenter vmwareDC = findVmwareDatacenterForVM(vm); + try { + if (!client.removeVMFromVeeamJob(vm.getBackupExternalId(), vm.getInstanceName(), vmwareDC.getVcenterHost())) { + LOG.warn("Failed to remove VM from Veeam Job id: " + vm.getBackupExternalId()); + } + } catch (Exception e) { + LOG.debug("VM was removed from the job so could not remove again, trying to delete the veeam job now.", e); + } + + final String clonedJobName = getGuestBackupName(vm.getInstanceName(), vm.getUuid()); + if (!client.deleteJobAndBackup(clonedJobName)) { + LOG.warn("Failed to remove Veeam job and backup for job: " + clonedJobName); + throw new CloudRuntimeException("Failed to delete Veeam B&R job and backup, an operation may be in progress. Please try again after some time."); + } + return true; + } + + @Override + public boolean willDeleteBackupsOnOfferingRemoval() { + return true; + } + + @Override + public boolean takeBackup(final VirtualMachine vm) { + final VeeamClient client = getClient(vm.getDataCenterId()); + return client.startBackupJob(vm.getBackupExternalId()); + } + + @Override + public boolean deleteBackup(Backup backup) { + // Veeam does not support removal of a restore point or point-in-time backup + throw new CloudRuntimeException("Veeam B&R plugin does not allow removal of backup restore point, to delete the backup chain remove VM from the backup offering"); + } + + @Override + public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { + final String restorePointId = backup.getExternalId(); + return getClient(vm.getDataCenterId()).restoreFullVM(vm.getInstanceName(), restorePointId); + } + + @Override + public Pair restoreBackedUpVolume(Backup backup, String volumeUuid, String hostIp, String dataStoreUuid) { + final Long zoneId = backup.getZoneId(); + final String restorePointId = backup.getExternalId(); + return getClient(zoneId).restoreVMToDifferentLocation(restorePointId, hostIp, dataStoreUuid); + } + + @Override + public Map getBackupMetrics(final Long zoneId, final List vms) { + final Map metrics = new HashMap<>(); + final Map backendMetrics = getClient(zoneId).getBackupMetrics(); + for (final VirtualMachine vm : vms) { + if (!backendMetrics.containsKey(vm.getUuid())) { + continue; + } + metrics.put(vm, backendMetrics.get(vm.getUuid())); + } + return metrics; + } + + private List listRestorePoints(VirtualMachine vm) { + String backupName = getGuestBackupName(vm.getInstanceName(), vm.getUuid()); + return getClient(vm.getDataCenterId()).listRestorePoints(backupName, vm.getInstanceName()); + } + + @Override + public void syncBackups(VirtualMachine vm, Backup.Metric metric) { + List restorePoints = listRestorePoints(vm); + if (restorePoints == null || restorePoints.isEmpty()) { + return; + } + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + final List backupsInDb = backupDao.listByVmId(null, vm.getId()); + final List removeList = backupsInDb.stream().map(InternalIdentity::getId).collect(Collectors.toList()); + for (final Backup.RestorePoint restorePoint : restorePoints) { + boolean backupExists = false; + for (final Backup backup : backupsInDb) { + if (restorePoint.getId().equals(backup.getExternalId())) { + backupExists = true; + removeList.remove(backup.getId()); + if (metric != null) { + ((BackupVO) backup).setSize(metric.getBackupSize()); + ((BackupVO) backup).setProtectedSize(metric.getDataSize()); + backupDao.update(backup.getId(), ((BackupVO) backup)); + } + break; + } + } + if (backupExists) { + continue; + } + BackupVO backup = new BackupVO(); + backup.setVmId(vm.getId()); + backup.setExternalId(restorePoint.getId()); + backup.setType(restorePoint.getType()); + backup.setDate(restorePoint.getCreated()); + backup.setStatus(Backup.Status.BackedUp); + if (metric != null) { + backup.setSize(metric.getBackupSize()); + backup.setProtectedSize(metric.getDataSize()); + } + backup.setBackupOfferingId(vm.getBackupOfferingId()); + backup.setAccountId(vm.getAccountId()); + backup.setDomainId(vm.getDomainId()); + backup.setZoneId(vm.getDataCenterId()); + backupDao.persist(backup); + } + for (final Long backupIdToRemove : removeList) { + backupDao.remove(backupIdToRemove); + } + } + }); + } + + @Override + public String getConfigComponentName() { + return BackupService.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + VeeamUrl, + VeeamUsername, + VeeamPassword, + VeeamValidateSSLSecurity, + VeeamApiRequestTimeout + }; + } + + @Override + public String getName() { + return "veeam"; + } + + @Override + public String getDescription() { + return "Veeam Backup Plugin"; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamBackupOffering.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamBackupOffering.java new file mode 100644 index 00000000000..9b1e7b0322c --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamBackupOffering.java @@ -0,0 +1,78 @@ +// 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.backup.veeam; + +import java.util.Date; + +import org.apache.cloudstack.backup.BackupOffering; + +public class VeeamBackupOffering implements BackupOffering { + + private String name; + private String uid; + + public VeeamBackupOffering(String name, String uid) { + this.name = name; + this.uid = uid; + } + + @Override + public String getExternalId() { + return uid; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return "Veeam Backup Offering (Job)"; + } + + @Override + public long getZoneId() { + return -1; + } + + @Override + public boolean isUserDrivenBackupAllowed() { + return false; + } + + @Override + public String getProvider() { + return "veeam"; + } + + @Override + public Date getCreated() { + return null; + } + + @Override + public String getUuid() { + return uid; + } + + @Override + public long getId() { + return -1; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java new file mode 100644 index 00000000000..1c0cc72c16a --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamClient.java @@ -0,0 +1,654 @@ +// 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.backup.veeam; + +import static org.apache.cloudstack.backup.VeeamBackupProvider.BACKUP_IDENTIFIER; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.UUID; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.veeam.api.BackupJobCloneInfo; +import org.apache.cloudstack.backup.veeam.api.CreateObjectInJobSpec; +import org.apache.cloudstack.backup.veeam.api.EntityReferences; +import org.apache.cloudstack.backup.veeam.api.HierarchyItem; +import org.apache.cloudstack.backup.veeam.api.HierarchyItems; +import org.apache.cloudstack.backup.veeam.api.Job; +import org.apache.cloudstack.backup.veeam.api.JobCloneSpec; +import org.apache.cloudstack.backup.veeam.api.Link; +import org.apache.cloudstack.backup.veeam.api.ObjectInJob; +import org.apache.cloudstack.backup.veeam.api.ObjectsInJob; +import org.apache.cloudstack.backup.veeam.api.Ref; +import org.apache.cloudstack.backup.veeam.api.RestoreSession; +import org.apache.cloudstack.backup.veeam.api.Task; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.log4j.Logger; + +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.nio.TrustAllManager; +import com.cloud.utils.ssh.SshHelper; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import com.google.common.base.Strings; + +public class VeeamClient { + private static final Logger LOG = Logger.getLogger(VeeamClient.class); + + private final URI apiURI; + + private final HttpClient httpClient; + private static final String RESTORE_VM_SUFFIX = "CS-RSTR-"; + private static final String SESSION_HEADER = "X-RestSvcSessionId"; + + private String veeamServerIp; + private String veeamServerUsername; + private String veeamServerPassword; + private String veeamSessionId = null; + private final int veeamServerPort = 22; + + public VeeamClient(final String url, final String username, final String password, final boolean validateCertificate, final int timeout) throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException { + this.apiURI = new URI(url); + + final RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000) + .setSocketTimeout(timeout * 1000) + .build(); + + if (!validateCertificate) { + final SSLContext sslcontext = SSLUtils.getSSLContext(); + sslcontext.init(null, new X509TrustManager[]{new TrustAllManager()}, new SecureRandom()); + final SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE); + this.httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .setSSLSocketFactory(factory) + .build(); + } else { + this.httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .build(); + } + + authenticate(username, password); + setVeeamSshCredentials(this.apiURI.getHost(), username, password); + } + + protected void setVeeamSshCredentials(String hostIp, String username, String password) { + this.veeamServerIp = hostIp; + this.veeamServerUsername = username; + this.veeamServerPassword = password; + } + + private void authenticate(final String username, final String password) { + // https://helpcenter.veeam.com/docs/backup/rest/http_authentication.html?ver=95u4 + final HttpPost request = new HttpPost(apiURI.toString() + "/sessionMngr/?v=v1_4"); + request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())); + try { + final HttpResponse response = httpClient.execute(request); + checkAuthFailure(response); + veeamSessionId = response.getFirstHeader(SESSION_HEADER).getValue(); + if (Strings.isNullOrEmpty(veeamSessionId)) { + throw new CloudRuntimeException("Veeam Session ID is not available to perform API requests"); + } + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_CREATED) { + throw new CloudRuntimeException("Failed to create and authenticate Veeam API client, please check the settings."); + } + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to authenticate Veeam API service due to:" + e.getMessage()); + } + } + + private void checkAuthFailure(final HttpResponse response) { + if (response != null && response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { + throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, "Veeam B&R API call unauthorized, please ask your administrator to fix integration issues."); + } + } + + private void checkResponseOK(final HttpResponse response) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) { + LOG.debug("Requested Veeam resource does not exist"); + return; + } + if (!(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK || + response.getStatusLine().getStatusCode() == HttpStatus.SC_ACCEPTED) && + response.getStatusLine().getStatusCode() != HttpStatus.SC_NO_CONTENT) { + LOG.debug("HTTP request failed, status code is " + response.getStatusLine().getStatusCode() + ", response is: " + response.toString()); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Got invalid API status code returned by the Veeam server"); + } + } + + private void checkResponseTimeOut(final Exception e) { + if (e instanceof ConnectTimeoutException || e instanceof SocketTimeoutException) { + throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, "Veeam API operation timed out, please try again."); + } + } + + private HttpResponse get(final String path) throws IOException { + final HttpGet request = new HttpGet(apiURI.toString() + path); + request.setHeader(SESSION_HEADER, veeamSessionId); + final HttpResponse response = httpClient.execute(request); + checkAuthFailure(response); + return response; + } + + private HttpResponse post(final String path, final Object obj) throws IOException { + String xml = null; + if (obj != null) { + XmlMapper xmlMapper = new XmlMapper(); + xml = xmlMapper.writer() + .with(ToXmlGenerator.Feature.WRITE_XML_DECLARATION) + .writeValueAsString(obj); + // Remove invalid/empty xmlns + xml = xml.replace(" xmlns=\"\"", ""); + } + + final HttpPost request = new HttpPost(apiURI.toString() + path); + request.setHeader(SESSION_HEADER, veeamSessionId); + request.setHeader("Content-type", "application/xml"); + if (StringUtils.isNotBlank(xml)) { + request.setEntity(new StringEntity(xml)); + } + + final HttpResponse response = httpClient.execute(request); + checkAuthFailure(response); + return response; + } + + private HttpResponse delete(final String path) throws IOException { + final HttpDelete request = new HttpDelete(apiURI.toString() + path); + request.setHeader(SESSION_HEADER, veeamSessionId); + final HttpResponse response = httpClient.execute(request); + checkAuthFailure(response); + return response; + } + + /////////////////////////////////////////////////////////////////// + //////////////// Private Veeam Helper Methods ///////////////////// + /////////////////////////////////////////////////////////////////// + + private String findDCHierarchy(final String vmwareDcName) { + LOG.debug("Trying to find hierarchy ID for vmware datacenter: " + vmwareDcName); + + try { + final HttpResponse response = get("/hierarchyRoots"); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + final EntityReferences references = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class); + for (final Ref ref : references.getRefs()) { + if (ref.getName().equals(vmwareDcName) && ref.getType().equals("HierarchyRootReference")) { + return ref.getUid(); + } + } + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } + throw new CloudRuntimeException("Failed to find hierarchy reference for VMware datacenter " + vmwareDcName + " in Veeam, please ask administrator to check Veeam B&R manager configuration"); + } + + private String lookupVM(final String hierarchyId, final String vmName) { + LOG.debug("Trying to lookup VM from veeam hierarchy:" + hierarchyId + " for vm name:" + vmName); + + try { + final HttpResponse response = get(String.format("/lookup?host=%s&type=Vm&name=%s", hierarchyId, vmName)); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + final HierarchyItems items = objectMapper.readValue(response.getEntity().getContent(), HierarchyItems.class); + if (items == null || items.getItems() == null || items.getItems().isEmpty()) { + throw new CloudRuntimeException("Could not find VM " + vmName + " in Veeam, please ask administrator to check Veeam B&R manager"); + } + for (final HierarchyItem item : items.getItems()) { + if (item.getObjectName().equals(vmName) && item.getObjectType().equals("Vm")) { + return item.getObjectRef(); + } + } + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } + throw new CloudRuntimeException("Failed to lookup VM " + vmName + " in Veeam, please ask administrator to check Veeam B&R manager configuration"); + } + + private Task parseTaskResponse(HttpResponse response) throws IOException { + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + return objectMapper.readValue(response.getEntity().getContent(), Task.class); + } + + private RestoreSession parseRestoreSessionResponse(HttpResponse response) throws IOException { + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + return objectMapper.readValue(response.getEntity().getContent(), RestoreSession.class); + } + + private boolean checkTaskStatus(final HttpResponse response) throws IOException { + final Task task = parseTaskResponse(response); + for (int i = 0; i < 120; i++) { + final HttpResponse taskResponse = get("/tasks/" + task.getTaskId()); + final Task polledTask = parseTaskResponse(taskResponse); + if (polledTask.getState().equals("Finished")) { + final HttpResponse taskDeleteResponse = delete("/tasks/" + task.getTaskId()); + if (taskDeleteResponse.getStatusLine().getStatusCode() != HttpStatus.SC_NO_CONTENT) { + LOG.warn("Operation failed for veeam task id=" + task.getTaskId()); + } + if (polledTask.getResult().getSuccess().equals("true")) { + Pair pair = getRelatedLinkPair(polledTask.getLink()); + if (pair != null) { + String url = pair.first(); + String type = pair.second(); + String path = url.replace(apiURI.toString(), ""); + if (type.equals("RestoreSession")) { + for (int j = 0; j < 120; j++) { + HttpResponse relatedResponse = get(path); + RestoreSession session = parseRestoreSessionResponse(relatedResponse); + if (session.getResult().equals("Success")) { + return true; + } + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + } + throw new CloudRuntimeException("Related job type: " + type + " was not successful"); + } + } + return true; + } + throw new CloudRuntimeException("Failed to assign VM to backup offering due to: " + polledTask.getResult().getMessage()); + } + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + LOG.debug("Failed to sleep while polling for Veeam task status due to: ", e); + } + } + return false; + } + + private Pair getRelatedLinkPair(List links) { + for (Link link : links) { + if (link.getRel().equals("Related")) { + return new Pair<>(link.getHref(), link.getType()); + } + } + return null; + } + + //////////////////////////////////////////////////////// + //////////////// Public Veeam APIs ///////////////////// + //////////////////////////////////////////////////////// + + public Ref listBackupRepository(final String backupServerId) { + LOG.debug("Trying to list backup repository for backup server id: " + backupServerId); + try { + final HttpResponse response = get(String.format("/backupServers/%s/repositories", backupServerId)); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + final EntityReferences references = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class); + for (final Ref ref : references.getRefs()) { + if (ref.getType().equals("RepositoryReference")) { + return ref; + } + } + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } + return null; + } + + public void listAllBackups() { + LOG.debug("Trying to list Veeam backups"); + try { + final HttpResponse response = get("/backups"); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + final EntityReferences entityReferences = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class); + for (final Ref ref : entityReferences.getRefs()) { + LOG.debug("Veeam Backup found, name: " + ref.getName() + ", uid: " + ref.getUid() + ", type: " + ref.getType()); + } + } catch (final IOException e) { + LOG.error("Failed to list Veeam backups due to:", e); + checkResponseTimeOut(e); + } + } + + public List listJobs() { + LOG.debug("Trying to list backup policies that are Veeam jobs"); + try { + final HttpResponse response = get("/jobs"); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + final EntityReferences entityReferences = objectMapper.readValue(response.getEntity().getContent(), EntityReferences.class); + final List policies = new ArrayList<>(); + if (entityReferences == null || entityReferences.getRefs() == null) { + return policies; + } + for (final Ref ref : entityReferences.getRefs()) { + policies.add(new VeeamBackupOffering(ref.getName(), ref.getUid())); + } + return policies; + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } + return new ArrayList<>(); + } + + public Job listJob(final String jobId) { + LOG.debug("Trying to list veeam job id: " + jobId); + try { + final HttpResponse response = get(String.format("/jobs/%s?format=Entity", + jobId.replace("urn:veeam:Job:", ""))); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return objectMapper.readValue(response.getEntity().getContent(), Job.class); + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } catch (final ServerApiException e) { + LOG.error(e); + } + return null; + } + + public boolean toggleJobSchedule(final String jobId) { + LOG.debug("Trying to toggle schedule for Veeam job: " + jobId); + try { + final HttpResponse response = post(String.format("/jobs/%s?action=toggleScheduleEnabled", jobId), null); + return checkTaskStatus(response); + } catch (final IOException e) { + LOG.error("Failed to toggle Veeam job schedule due to:", e); + checkResponseTimeOut(e); + } + return false; + } + + public boolean startBackupJob(final String jobId) { + LOG.debug("Trying to start ad-hoc backup for Veeam job: " + jobId); + try { + final HttpResponse response = post(String.format("/jobs/%s?action=start", jobId), null); + return checkTaskStatus(response); + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } + return false; + } + + public boolean cloneVeeamJob(final Job parentJob, final String clonedJobName) { + LOG.debug("Trying to clone veeam job: " + parentJob.getUid() + " with backup uuid: " + clonedJobName); + try { + final Ref repositoryRef = listBackupRepository(parentJob.getBackupServerId()); + final BackupJobCloneInfo cloneInfo = new BackupJobCloneInfo(); + cloneInfo.setJobName(clonedJobName); + cloneInfo.setFolderName(clonedJobName); + cloneInfo.setRepositoryUid(repositoryRef.getUid()); + final JobCloneSpec cloneSpec = new JobCloneSpec(cloneInfo); + final HttpResponse response = post(String.format("/jobs/%s?action=clone", parentJob.getId()), cloneSpec); + return checkTaskStatus(response); + } catch (final Exception e) { + LOG.warn("Exception caught while trying to clone Veeam job:", e); + } + return false; + } + + public boolean addVMToVeeamJob(final String jobId, final String vmwareInstanceName, final String vmwareDcName) { + LOG.debug("Trying to add VM to backup offering that is Veeam job: " + jobId); + try { + final String heirarchyId = findDCHierarchy(vmwareDcName); + final String veeamVmRefId = lookupVM(heirarchyId, vmwareInstanceName); + final CreateObjectInJobSpec vmToBackupJob = new CreateObjectInJobSpec(); + vmToBackupJob.setObjName(vmwareInstanceName); + vmToBackupJob.setObjRef(veeamVmRefId); + final HttpResponse response = post(String.format("/jobs/%s/includes", jobId), vmToBackupJob); + return checkTaskStatus(response); + } catch (final IOException e) { + LOG.error("Failed to add VM to Veeam job due to:", e); + checkResponseTimeOut(e); + } + throw new CloudRuntimeException("Failed to add VM to backup offering likely due to timeout, please check Veeam tasks"); + } + + public boolean removeVMFromVeeamJob(final String jobId, final String vmwareInstanceName, final String vmwareDcName) { + LOG.debug("Trying to remove VM from backup offering that is a Veeam job: " + jobId); + try { + final String hierarchyId = findDCHierarchy(vmwareDcName); + final String veeamVmRefId = lookupVM(hierarchyId, vmwareInstanceName); + final HttpResponse response = get(String.format("/jobs/%s/includes", jobId)); + checkResponseOK(response); + final ObjectMapper objectMapper = new XmlMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + final ObjectsInJob jobObjects = objectMapper.readValue(response.getEntity().getContent(), ObjectsInJob.class); + if (jobObjects == null || jobObjects.getObjects() == null) { + LOG.warn("No objects found in the Veeam job " + jobId); + return false; + } + for (final ObjectInJob jobObject : jobObjects.getObjects()) { + if (jobObject.getName().equals(vmwareInstanceName) && jobObject.getHierarchyObjRef().equals(veeamVmRefId)) { + final HttpResponse deleteResponse = delete(String.format("/jobs/%s/includes/%s", jobId, jobObject.getObjectInJobId())); + return checkTaskStatus(deleteResponse); + } + } + LOG.warn(vmwareInstanceName + " VM was not found to be attached to Veaam job (backup offering): " + jobId); + return false; + } catch (final IOException e) { + LOG.error("Failed to list Veeam jobs due to:", e); + checkResponseTimeOut(e); + } + return false; + } + + public boolean restoreFullVM(final String vmwareInstanceName, final String restorePointId) { + LOG.debug("Trying to restore full VM: " + vmwareInstanceName + " from backup"); + try { + final HttpResponse response = post(String.format("/vmRestorePoints/%s?action=restore", restorePointId), null); + return checkTaskStatus(response); + } catch (final IOException e) { + LOG.error("Failed to restore full VM due to: ", e); + checkResponseTimeOut(e); + } + throw new CloudRuntimeException("Failed to restore full VM from backup"); + } + + ///////////////////////////////////////////////////////////////// + //////////////// Public Veeam PS based APIs ///////////////////// + ///////////////////////////////////////////////////////////////// + + /** + * Generate a single command to be passed through SSH + */ + protected String transformPowerShellCommandList(List cmds) { + StringJoiner joiner = new StringJoiner(";"); + joiner.add("PowerShell Add-PSSnapin VeeamPSSnapin"); + for (String cmd : cmds) { + joiner.add(cmd); + } + return joiner.toString(); + } + + /** + * Execute a list of commands in a single call on PowerShell through SSH + */ + protected Pair executePowerShellCommands(List cmds) { + try { + Pair pairResult = SshHelper.sshExecute(veeamServerIp, veeamServerPort, + veeamServerUsername, null, veeamServerPassword, + transformPowerShellCommandList(cmds), + 120000, 120000, 3600000); + return pairResult; + } catch (Exception e) { + throw new CloudRuntimeException("Error while executing PowerShell commands due to: " + e.getMessage()); + } + } + + public boolean setJobSchedule(final String jobName) { + Pair result = executePowerShellCommands(Arrays.asList( + String.format("$job = Get-VBRJob -Name \"%s\"", jobName), + "if ($job) { Set-VBRJobSchedule -Job $job -Daily -At \"11:00\" -DailyKind Weekdays }" + )); + return result.first() && !result.second().isEmpty() && !result.second().contains("Failed to delete"); + } + + public boolean deleteJobAndBackup(final String jobName) { + Pair result = executePowerShellCommands(Arrays.asList( + String.format("$job = Get-VBRJob -Name \"%s\"", jobName), + "if ($job) { Remove-VBRJob -Job $job -Confirm:$false }", + String.format("$backup = Get-VBRBackup -Name \"%s\"", jobName), + "if ($backup) { Remove-VBRBackup -Backup $backup -FromDisk -Confirm:$false }", + "$repo = Get-VBRBackupRepository", + "Sync-VBRBackupRepository -Repository $repo" + )); + return result.first() && !result.second().contains("Failed to delete"); + } + + public Map getBackupMetrics() { + final String separator = "====="; + final List cmds = Arrays.asList( + "$backups = Get-VBRBackup", + "foreach ($backup in $backups) {" + + "$backup.JobName;" + + "$storageGroups = $backup.GetStorageGroups();" + + "foreach ($group in $storageGroups) {" + + "$usedSize = 0;" + + "$dataSize = 0;" + + "$sizePerStorage = $group.GetStorages().Stats.BackupSize;" + + "$dataPerStorage = $group.GetStorages().Stats.DataSize;" + + "foreach ($size in $sizePerStorage) {" + + "$usedSize += $size;" + + "}" + + "foreach ($size in $dataPerStorage) {" + + "$dataSize += $size;" + + "}" + + "$usedSize;" + + "$dataSize;" + + "}" + + "echo \"" + separator + "\"" + + "}" + ); + Pair response = executePowerShellCommands(cmds); + final Map sizes = new HashMap<>(); + for (final String block : response.second().split(separator + "\r\n")) { + final String[] parts = block.split("\r\n"); + if (parts.length != 3) { + continue; + } + final String backupName = parts[0]; + if (backupName != null && backupName.contains(BACKUP_IDENTIFIER)) { + final String[] names = backupName.split(BACKUP_IDENTIFIER); + sizes.put(names[names.length - 1], new Backup.Metric(Long.valueOf(parts[1]), Long.valueOf(parts[2]))); + } + } + return sizes; + } + + private Backup.RestorePoint getRestorePointFromBlock(String[] parts) { + String id = null; + String created = null; + String type = null; + for (String part : parts) { + if (part.matches("Id(\\s)+:(.)*")) { + String[] split = part.split(":"); + id = split[1].trim(); + } else if (part.matches("CreationTime(\\s)+:(.)*")) { + String [] split = part.split(":", 2); + created = split[1].trim(); + } else if (part.matches("Type(\\s)+:(.)*")) { + String [] split = part.split(":"); + type = split[1].trim(); + } + } + return new Backup.RestorePoint(id, created, type); + } + + public List listRestorePoints(String backupName, String vmInternalName) { + final List cmds = Arrays.asList( + String.format("$backup = Get-VBRBackup -Name \"%s\"", backupName), + String.format("if ($backup) { (Get-VBRRestorePoint -Backup:$backup -Name \"%s\" ^| Where-Object {$_.IsConsistent -eq $true}) }", vmInternalName) + ); + Pair response = executePowerShellCommands(cmds); + final List restorePoints = new ArrayList<>(); + if (response == null || !response.first()) { + LOG.debug("Veeam restore point listing failed due to: " + (response != null ? response.second() : "no powershell output returned")); + return restorePoints; + } + for (final String block : response.second().split("\r\n\r\n")) { + if (block.isEmpty()) { + continue; + } + final String[] parts = block.split("\r\n"); + restorePoints.add(getRestorePointFromBlock(parts)); + } + return restorePoints; + } + + public Pair restoreVMToDifferentLocation(String restorePointId, String hostIp, String dataStoreUuid) { + final String restoreLocation = RESTORE_VM_SUFFIX + UUID.randomUUID().toString(); + final String datastoreId = dataStoreUuid.replace("-",""); + final List cmds = Arrays.asList( + "$points = Get-VBRRestorePoint", + String.format("foreach($point in $points) { if ($point.Id -eq '%s') { break; } }", restorePointId), + String.format("$server = Get-VBRServer -Name \"%s\"", hostIp), + String.format("$ds = Find-VBRViDatastore -Server:$server -Name \"%s\"", datastoreId), + String.format("$job = Start-VBRRestoreVM -RestorePoint:$point -Server:$server -Datastore:$ds -VMName \"%s\" -RunAsync", restoreLocation), + "while (-not (Get-VBRRestoreSession -Id $job.Id).IsCompleted) { Start-Sleep -Seconds 10 }" + ); + Pair result = executePowerShellCommands(cmds); + if (result == null || !result.first()) { + throw new CloudRuntimeException("Failed to restore VM to location " + restoreLocation); + } + return new Pair<>(result.first(), restoreLocation); + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamObject.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamObject.java new file mode 100644 index 00000000000..6ecf08081d8 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/VeeamObject.java @@ -0,0 +1,28 @@ +// 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.backup.veeam; + +import java.util.List; + +public interface VeeamObject { + String getUuid(); + String getName(); + String getHref(); + String getType(); + List getLinks(); +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupJobCloneInfo.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupJobCloneInfo.java new file mode 100644 index 00000000000..fdb6081c6fe --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/BackupJobCloneInfo.java @@ -0,0 +1,58 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "BackupJobCloneInfo") +public class BackupJobCloneInfo { + + @JacksonXmlProperty(localName = "JobName") + private String jobName; + + @JacksonXmlProperty(localName = "FolderName") + private String folderName; + + @JacksonXmlProperty(localName = "RepositoryUid") + private String repositoryUid; + + public String getJobName() { + return jobName; + } + + public void setJobName(String jobName) { + this.jobName = jobName; + } + + public String getFolderName() { + return folderName; + } + + public void setFolderName(String folderName) { + this.folderName = folderName; + } + + public String getRepositoryUid() { + return repositoryUid; + } + + public void setRepositoryUid(String repositoryUid) { + this.repositoryUid = repositoryUid; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/CreateObjectInJobSpec.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/CreateObjectInJobSpec.java new file mode 100644 index 00000000000..16de447d3c0 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/CreateObjectInJobSpec.java @@ -0,0 +1,46 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "CreateObjectInJobSpec", namespace = "http://www.veeam.com/ent/v1.0") +public class CreateObjectInJobSpec { + @JacksonXmlProperty(localName = "HierarchyObjRef") + String objRef; + + @JacksonXmlProperty(localName = "HierarchyObjName") + String objName; + + public String getObjRef() { + return objRef; + } + + public void setObjRef(String objRef) { + this.objRef = objRef; + } + + public String getObjName() { + return objName; + } + + public void setObjName(String objName) { + this.objName = objName; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/EntityReferences.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/EntityReferences.java new file mode 100644 index 00000000000..928e0dae4a5 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/EntityReferences.java @@ -0,0 +1,39 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "EntityReferences") +public class EntityReferences { + @JacksonXmlProperty(localName = "Ref") + @JacksonXmlElementWrapper(localName = "Ref", useWrapping = false) + private List refs; + + public List getRefs() { + return refs; + } + + public void setRefs(List refs) { + this.refs = refs; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/HierarchyItem.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/HierarchyItem.java new file mode 100644 index 00000000000..46f0e5e420c --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/HierarchyItem.java @@ -0,0 +1,68 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "HierarchyItem") +public class HierarchyItem { + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "ObjectRef") + private String objectRef; + + @JacksonXmlProperty(localName = "ObjectType") + private String objectType; + + @JacksonXmlProperty(localName = "ObjectName") + private String objectName; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getObjectRef() { + return objectRef; + } + + public void setObjectRef(String objectRef) { + this.objectRef = objectRef; + } + + public String getObjectType() { + return objectType; + } + + public void setObjectType(String objectType) { + this.objectType = objectType; + } + + public String getObjectName() { + return objectName; + } + + public void setObjectName(String objectName) { + this.objectName = objectName; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/HierarchyItems.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/HierarchyItems.java new file mode 100644 index 00000000000..a87c6b0f983 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/HierarchyItems.java @@ -0,0 +1,39 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "HierarchyItems") +public class HierarchyItems { + @JacksonXmlProperty(localName = "HierarchyItem") + @JacksonXmlElementWrapper(localName = "HierarchyItem", useWrapping = false) + private List items; + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Job.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Job.java new file mode 100644 index 00000000000..e42ecf0d4cc --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Job.java @@ -0,0 +1,163 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "Job") +public class Job { + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "UID", isAttribute = true) + private String uid; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "Platform") + private String platform; + + @JacksonXmlProperty(localName = "Description") + private String description; + + @JacksonXmlProperty(localName = "NextRun") + private String nextRun; + + @JacksonXmlProperty(localName = "JobType") + private String jobType; + + @JacksonXmlProperty(localName = "ScheduleEnabled") + private Boolean scheduleEnabled; + + @JacksonXmlProperty(localName = "ScheduleConfigured") + private Boolean scheduleConfigured; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUid() { + return uid; + } + + public String getId() { + return uid.replace("urn:veeam:Job:", ""); + } + + public void setUid(String uid) { + this.uid = uid; + } + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getNextRun() { + return nextRun; + } + + public void setNextRun(String nextRun) { + this.nextRun = nextRun; + } + + public String getJobType() { + return jobType; + } + + public void setJobType(String jobType) { + this.jobType = jobType; + } + + public String getBackupServerId() { + for (final Link l : link) { + if (l.getType().equals("BackupServerReference")) { + return "" + l.getHref().split("backupServers/")[1]; + } + } + return null; + } + + public Boolean getScheduleEnabled() { + return scheduleEnabled; + } + + public void setScheduleEnabled(String scheduleEnabled) { + this.scheduleEnabled = Boolean.valueOf(scheduleEnabled); + } + + public Boolean getScheduleConfigured() { + return scheduleConfigured; + } + + public void setScheduleConfigured(String scheduleConfigured) { + this.scheduleConfigured = Boolean.valueOf(scheduleConfigured); + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/JobCloneSpec.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/JobCloneSpec.java new file mode 100644 index 00000000000..fd1bdfbdedf --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/JobCloneSpec.java @@ -0,0 +1,41 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "JobCloneSpec", namespace = "http://www.veeam.com/ent/v1.0") +public class JobCloneSpec { + @JacksonXmlProperty(localName = "BackupJobCloneInfo") + @JacksonXmlElementWrapper(localName = "BackupJobCloneInfo", useWrapping = false) + BackupJobCloneInfo jobCloneInfo; + + public JobCloneSpec(final BackupJobCloneInfo jobCloneInfo) { + this.jobCloneInfo = jobCloneInfo; + } + + public BackupJobCloneInfo getJobCloneInfo() { + return jobCloneInfo; + } + + public void setJobCloneInfo(BackupJobCloneInfo jobCloneInfo) { + this.jobCloneInfo = jobCloneInfo; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Link.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Link.java new file mode 100644 index 00000000000..b89d77fa550 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Link.java @@ -0,0 +1,69 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "Link") +public class Link { + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Rel", isAttribute = true) + private String rel; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getRel() { + return rel; + } + + public void setRel(String rel) { + this.rel = rel; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/ObjectInJob.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/ObjectInJob.java new file mode 100644 index 00000000000..987176e59f8 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/ObjectInJob.java @@ -0,0 +1,94 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "ObjectInJob") +public class ObjectInJob { + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "ObjectInJobId", isAttribute = true) + private String objectInJobId; + + @JacksonXmlProperty(localName = "HierarchyObjRef", isAttribute = true) + private String hierarchyObjRef; + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getObjectInJobId() { + return objectInJobId; + } + + public void setObjectInJobId(String objectInJobId) { + this.objectInJobId = objectInJobId; + } + + public String getHierarchyObjRef() { + return hierarchyObjRef; + } + + public void setHierarchyObjRef(String hierarchyObjRef) { + this.hierarchyObjRef = hierarchyObjRef; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/ObjectsInJob.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/ObjectsInJob.java new file mode 100644 index 00000000000..982dae592dc --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/ObjectsInJob.java @@ -0,0 +1,39 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "ObjectsInJob") +public class ObjectsInJob { + @JacksonXmlProperty(localName = "ObjectInJob") + @JacksonXmlElementWrapper(localName = "ObjectInJob", useWrapping = false) + private List objects; + + public List getObjects() { + return objects; + } + + public void setObjects(List objects) { + this.objects = objects; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Ref.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Ref.java new file mode 100644 index 00000000000..683fd3773f6 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Ref.java @@ -0,0 +1,83 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "Ref") +public class Ref { + @JacksonXmlProperty(localName = "UID", isAttribute = true) + private String uid; + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestoreSession.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestoreSession.java new file mode 100644 index 00000000000..0675e49fc27 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/RestoreSession.java @@ -0,0 +1,120 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +import java.util.List; + +@JacksonXmlRootElement(localName = "RestoreSession") +public class RestoreSession { + + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Name", isAttribute = true) + private String name; + + @JacksonXmlProperty(localName = "UID", isAttribute = true) + private String uid; + + @JacksonXmlProperty(localName = "VmDisplayName", isAttribute = true) + private String vmDisplayName; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "JobType") + private String jobType; + + @JacksonXmlProperty(localName = "CreationTimeUTC") + private String creationTimeUTC; + + @JacksonXmlProperty(localName = "EndTimeUTC") + private String endTimeUTC; + + @JacksonXmlProperty(localName = "State") + private String state; + + @JacksonXmlProperty(localName = "Result") + private String result; + + @JacksonXmlProperty(localName = "Progress") + private String progress; + + @JacksonXmlProperty(localName = "RestoredObjRef") + private String restoredObjRef; + + public List getLink() { + return link; + } + + public String getJobType() { + return jobType; + } + + public String getState() { + return state; + } + + public String getResult() { + return result; + } + + public String getType() { + return type; + } + + public String getHref() { + return href; + } + + public String getVmDisplayName() { + return vmDisplayName; + } + + public String getCreationTimeUTC() { + return creationTimeUTC; + } + + public String getEndTimeUTC() { + return endTimeUTC; + } + + public String getProgress() { + return progress; + } + + public String getName() { + return name; + } + + public String getUid() { + return uid; + } + + public String getRestoredObjRef() { + return restoredObjRef; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Result.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Result.java new file mode 100644 index 00000000000..26fc8634184 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Result.java @@ -0,0 +1,47 @@ +// 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.backup.veeam.api; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "Result") +public class Result { + + @JacksonXmlProperty(localName = "Success", isAttribute = true) + private String success; + + @JacksonXmlProperty(localName = "Message") + private String message; + + public String getSuccess() { + return success; + } + + public void setSuccess(String success) { + this.success = success; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Task.java b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Task.java new file mode 100644 index 00000000000..e8d14e57d48 --- /dev/null +++ b/plugins/backup/veeam/src/main/java/org/apache/cloudstack/backup/veeam/api/Task.java @@ -0,0 +1,106 @@ +// 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.backup.veeam.api; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JacksonXmlRootElement(localName = "CreateObjectInJobSpec") +public class Task { + @JacksonXmlProperty(localName = "Type", isAttribute = true) + private String type; + + @JacksonXmlProperty(localName = "Href", isAttribute = true) + private String href; + + @JacksonXmlProperty(localName = "Link") + @JacksonXmlElementWrapper(localName = "Links") + private List link; + + @JacksonXmlProperty(localName = "TaskId") + private String taskId; + + @JacksonXmlProperty(localName = "State") + private String state; + + @JacksonXmlProperty(localName = "Operation") + private String operation; + + @JacksonXmlProperty(localName = "Result") + @JacksonXmlElementWrapper(localName = "Result", useWrapping = false) + private Result result; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public List getLink() { + return link; + } + + public void setLink(List link) { + this.link = link; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public Result getResult() { + return result; + } + + public void setResult(Result result) { + this.result = result; + } +} diff --git a/plugins/backup/veeam/src/main/resources/META-INF/cloudstack/veeam/module.properties b/plugins/backup/veeam/src/main/resources/META-INF/cloudstack/veeam/module.properties new file mode 100644 index 00000000000..ee40b21a323 --- /dev/null +++ b/plugins/backup/veeam/src/main/resources/META-INF/cloudstack/veeam/module.properties @@ -0,0 +1,18 @@ +# 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. +name=veeam +parent=backup diff --git a/plugins/backup/veeam/src/main/resources/META-INF/cloudstack/veeam/spring-backup-veeam-context.xml b/plugins/backup/veeam/src/main/resources/META-INF/cloudstack/veeam/spring-backup-veeam-context.xml new file mode 100644 index 00000000000..f2403cf37a4 --- /dev/null +++ b/plugins/backup/veeam/src/main/resources/META-INF/cloudstack/veeam/spring-backup-veeam-context.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java new file mode 100644 index 00000000000..3af81d02777 --- /dev/null +++ b/plugins/backup/veeam/src/test/java/org/apache/cloudstack/backup/veeam/VeeamClientTest.java @@ -0,0 +1,85 @@ +// 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.backup.veeam; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; + +import java.util.List; + +import org.apache.cloudstack.backup.BackupOffering; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.github.tomakehurst.wiremock.client.BasicCredentials; +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +public class VeeamClientTest { + + private String adminUsername = "administrator"; + private String adminPassword = "password"; + private VeeamClient client; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(9399); + + @Before + public void setUp() throws Exception { + wireMockRule.stubFor(post(urlMatching(".*/sessionMngr/.*")) + .willReturn(aResponse() + .withStatus(201) + .withHeader("X-RestSvcSessionId", "some-session-auth-id") + .withBody(""))); + client = new VeeamClient("http://localhost:9399/api/", adminUsername, adminPassword, true, 60); + } + + @Test + public void testBasicAuth() { + verify(postRequestedFor(urlMatching(".*/sessionMngr/.*")) + .withBasicAuth(new BasicCredentials(adminUsername, adminPassword))); + } + + @Test + public void testVeeamJobs() { + wireMockRule.stubFor(get(urlMatching(".*/jobs")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/xml") + .withStatus(200) + .withBody("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""))); + List policies = client.listJobs(); + verify(getRequestedFor(urlMatching(".*/jobs"))); + Assert.assertEquals(policies.size(), 1); + Assert.assertEquals(policies.get(0).getName(), "ZONE1-GOLD"); + } +} \ No newline at end of file diff --git a/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java b/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java index 4516c7c2fb8..246d86d2712 100644 --- a/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java +++ b/plugins/hypervisors/simulator/src/main/java/com/cloud/simulator/SimulatorGuru.java @@ -16,22 +16,41 @@ // under the License. package com.cloud.simulator; +import java.util.Date; import java.util.Map; import javax.inject.Inject; +import org.apache.cloudstack.backup.Backup; + import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorGuru; import com.cloud.hypervisor.HypervisorGuruBase; import com.cloud.storage.GuestOSVO; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.GuestOSDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.VMInstanceDao; public class SimulatorGuru extends HypervisorGuruBase implements HypervisorGuru { @Inject GuestOSDao _guestOsDao; + @Inject + VMInstanceDao instanceDao; + + @Inject + VolumeDao volumeDao; + + @Inject + NicDao nicDao; + protected SimulatorGuru() { super(); } @@ -52,6 +71,25 @@ public class SimulatorGuru extends HypervisorGuruBase implements HypervisorGuru return to; } + @Override + public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, + String vmInternalName, Backup backup) { + VMInstanceVO vm = instanceDao.findVMByInstanceNameIncludingRemoved(vmInternalName); + if (vm.getRemoved() != null) { + vm.setState(VirtualMachine.State.Stopped); + vm.setPowerState(VirtualMachine.PowerState.PowerOff); + instanceDao.update(vm.getId(), vm); + instanceDao.unremove(vm.getId()); + } + for (final VolumeVO volume : volumeDao.findIncludingRemovedByInstanceAndType(vm.getId(), null)) { + volume.setState(Volume.State.Ready); + volume.setAttached(new Date()); + volumeDao.update(volume.getId(), volume); + volumeDao.unremove(volume.getId()); + } + return vm; + } + @Override public boolean trackVmHostChange() { return false; diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java index 072ab9f6fed..39c1e66e64d 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/guru/VMwareGuru.java @@ -19,6 +19,7 @@ package com.cloud.hypervisor.guru; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,32 +28,26 @@ import java.util.stream.Collectors; import javax.inject.Inject; -import com.cloud.agent.api.MigrateVmToPoolCommand; -import com.cloud.agent.api.UnregisterVMCommand; -import com.cloud.agent.api.storage.OVFPropertyTO; -import com.cloud.agent.api.to.VolumeTO; -import com.cloud.dc.ClusterDetailsDao; -import com.cloud.storage.StoragePool; -import com.cloud.storage.TemplateOVFPropertyVO; -import com.cloud.storage.VMTemplateStoragePoolVO; -import com.cloud.storage.VMTemplateStorageResourceAssoc; -import com.cloud.storage.dao.TemplateOVFPropertiesDao; -import com.cloud.storage.dao.VMTemplatePoolDao; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.storage.command.CopyCommand; -import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.StorageSubSystemCommand; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.cloud.agent.api.BackupSnapshotCommand; @@ -60,10 +55,13 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.CreatePrivateTemplateFromSnapshotCommand; import com.cloud.agent.api.CreatePrivateTemplateFromVolumeCommand; import com.cloud.agent.api.CreateVolumeFromSnapshotCommand; +import com.cloud.agent.api.MigrateVmToPoolCommand; import com.cloud.agent.api.UnregisterNicCommand; +import com.cloud.agent.api.UnregisterVMCommand; import com.cloud.agent.api.storage.CopyVolumeCommand; import com.cloud.agent.api.storage.CreateEntityDownloadURLCommand; import com.cloud.agent.api.storage.CreateVolumeOVACommand; +import com.cloud.agent.api.storage.OVFPropertyTO; import com.cloud.agent.api.storage.PrepareOVAPackingCommand; import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.DataStoreTO; @@ -71,8 +69,14 @@ import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.NicTO; import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.agent.api.to.VolumeTO; import com.cloud.cluster.ClusterManager; +import com.cloud.configuration.Resource; +import com.cloud.dc.ClusterDetailsDao; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; @@ -80,32 +84,61 @@ import com.cloud.host.dao.HostDetailsDao; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorGuru; import com.cloud.hypervisor.HypervisorGuruBase; +import com.cloud.hypervisor.vmware.VmwareDatacenterVO; +import com.cloud.hypervisor.vmware.VmwareDatacenterZoneMapVO; +import com.cloud.hypervisor.vmware.dao.VmwareDatacenterDao; +import com.cloud.hypervisor.vmware.dao.VmwareDatacenterZoneMapDao; import com.cloud.hypervisor.vmware.manager.VmwareManager; +import com.cloud.hypervisor.vmware.mo.DatacenterMO; import com.cloud.hypervisor.vmware.mo.DiskControllerType; +import com.cloud.hypervisor.vmware.mo.NetworkMO; +import com.cloud.hypervisor.vmware.mo.VirtualDiskManagerMO; import com.cloud.hypervisor.vmware.mo.VirtualEthernetCardType; +import com.cloud.hypervisor.vmware.mo.VirtualMachineDiskInfoBuilder; +import com.cloud.hypervisor.vmware.mo.VirtualMachineMO; +import com.cloud.hypervisor.vmware.resource.VmwareContextFactory; +import com.cloud.hypervisor.vmware.util.VmwareContext; +import com.cloud.network.Network; import com.cloud.network.Network.Provider; import com.cloud.network.Network.Service; import com.cloud.network.NetworkModel; +import com.cloud.network.Networks; import com.cloud.network.Networks.BroadcastDomainType; import com.cloud.network.Networks.TrafficType; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PhysicalNetworkDao; import com.cloud.network.dao.PhysicalNetworkTrafficTypeDao; import com.cloud.network.dao.PhysicalNetworkTrafficTypeVO; +import com.cloud.network.dao.PhysicalNetworkVO; import com.cloud.secstorage.CommandExecLogDao; import com.cloud.secstorage.CommandExecLogVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOSHypervisorVO; import com.cloud.storage.GuestOSVO; import com.cloud.storage.Storage; +import com.cloud.storage.StoragePool; +import com.cloud.storage.TemplateOVFPropertyVO; +import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.VMTemplateStorageResourceAssoc; +import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.GuestOSHypervisorDao; +import com.cloud.storage.dao.TemplateOVFPropertiesDao; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.secondary.SecondaryStorageVmManager; import com.cloud.template.VirtualMachineTemplate.BootloaderType; +import com.cloud.user.ResourceLimitService; import com.cloud.utils.Pair; +import com.cloud.utils.UuidUtils; import com.cloud.utils.db.DB; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; @@ -113,13 +146,28 @@ import com.cloud.vm.DomainRouterVO; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.SecondaryStorageVmVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.Type; +import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import com.google.gson.Gson; +import com.vmware.vim25.ManagedObjectReference; +import com.vmware.vim25.VirtualDevice; +import com.vmware.vim25.VirtualDeviceBackingInfo; +import com.vmware.vim25.VirtualDeviceConnectInfo; +import com.vmware.vim25.VirtualDisk; +import com.vmware.vim25.VirtualDiskFlatVer2BackingInfo; +import com.vmware.vim25.VirtualE1000; +import com.vmware.vim25.VirtualEthernetCardNetworkBackingInfo; +import com.vmware.vim25.VirtualMachineConfigSummary; +import com.vmware.vim25.VirtualMachineRuntimeInfo; public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Configurable { private static final Logger s_logger = Logger.getLogger(VMwareGuru.class); @@ -153,15 +201,33 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co @Inject private VMInstanceDao _vmDao; @Inject + private VirtualMachineManager vmManager; + @Inject private ClusterManager _clusterMgr; @Inject VolumeDao _volumeDao; @Inject + ResourceLimitService _resourceLimitService; + @Inject PrimaryDataStoreDao _storagePoolDao; @Inject VolumeDataFactory _volFactory; @Inject - private VMTemplatePoolDao templateSpoolDao; + private VmwareDatacenterDao vmwareDatacenterDao; + @Inject + private VmwareDatacenterZoneMapDao vmwareDatacenterZoneMapDao; + @Inject + private ServiceOfferingDao serviceOfferingDao; + @Inject + private VMTemplatePoolDao templateStoragePoolDao; + @Inject + private VMTemplateDao vmTemplateDao; + @Inject + private UserVmDao userVmDao; + @Inject + private DiskOfferingDao diskOfferingDao; + @Inject + private PhysicalNetworkDao physicalNetworkDao; @Inject private TemplateOVFPropertiesDao templateOVFPropertiesDao; @@ -398,7 +464,7 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co long dataCenterId = storagePoolVO.getDataCenterId(); List pools = _storagePoolDao.listByDataCenterId(dataCenterId); for (StoragePoolVO pool : pools) { - VMTemplateStoragePoolVO ref = templateSpoolDao.findByPoolTemplate(pool.getId(), vm.getTemplateId()); + VMTemplateStoragePoolVO ref = templateStoragePoolDao.findByPoolTemplate(pool.getId(), vm.getTemplateId()); if (ref != null && ref.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOADED) { templateInstallPath = ref.getInstallPath(); break; @@ -718,6 +784,671 @@ public class VMwareGuru extends HypervisorGuruBase implements HypervisorGuru, Co return details; } + /** + * Get vmware datacenter mapped to the zoneId + */ + private VmwareDatacenterVO getVmwareDatacenter(long zoneId) { + VmwareDatacenterZoneMapVO zoneMap = vmwareDatacenterZoneMapDao.findByZoneId(zoneId); + long vmwareDcId = zoneMap.getVmwareDcId(); + return vmwareDatacenterDao.findById(vmwareDcId); + } + + /** + * Get Vmware datacenter MO + */ + private DatacenterMO getDatacenterMO(long zoneId) throws Exception { + VmwareDatacenterVO vmwareDatacenter = getVmwareDatacenter(zoneId); + VmwareContext context = VmwareContextFactory.getContext(vmwareDatacenter.getVcenterHost(), + vmwareDatacenter.getUser(), vmwareDatacenter.getPassword()); + DatacenterMO dcMo = new DatacenterMO(context, vmwareDatacenter.getVmwareDatacenterName()); + ManagedObjectReference dcMor = dcMo.getMor(); + if (dcMor == null) { + String msg = "Error while getting Vmware datacenter " + vmwareDatacenter.getVmwareDatacenterName(); + s_logger.error(msg); + throw new InvalidParameterValueException(msg); + } + return dcMo; + } + + /** + * Get guest OS ID for VM being imported. + * If it cannot be found it is mapped to: "Other (64-bit)" ID + */ + private Long getImportingVMGuestOs(VirtualMachineConfigSummary configSummary) { + String guestFullName = configSummary.getGuestFullName(); + GuestOSVO os = _guestOsDao.listByDisplayName(guestFullName); + return os != null ? os.getId() : _guestOsDao.listByDisplayName("Other (64-bit)").getId(); + } + + /** + * Create and persist service offering + */ + private ServiceOfferingVO createServiceOfferingForVMImporting(Integer cpus, Integer memory, Integer maxCpuUsage) { + String name = "Imported-" + cpus + "-" + memory; + ServiceOfferingVO vo = new ServiceOfferingVO(name, cpus, memory, maxCpuUsage, null, null, + false, name, Storage.ProvisioningType.THIN, false, false, + null, false, Type.User, false); + return serviceOfferingDao.persist(vo); + } + + /** + * Get service offering ID for VM being imported. + * If it cannot be found it creates one and returns its ID + */ + private Long getImportingVMServiceOffering(VirtualMachineConfigSummary configSummary, + VirtualMachineRuntimeInfo runtimeInfo) { + Integer numCpu = configSummary.getNumCpu(); + Integer memorySizeMB = configSummary.getMemorySizeMB(); + Integer maxCpuUsage = runtimeInfo.getMaxCpuUsage(); + List offerings = serviceOfferingDao.listPublicByCpuAndMemory(numCpu, memorySizeMB); + return CollectionUtils.isEmpty(offerings) ? + createServiceOfferingForVMImporting(numCpu, memorySizeMB, maxCpuUsage).getId() : + offerings.get(0).getId(); + } + + /** + * Check if disk is ROOT disk + */ + private boolean isRootDisk(VirtualDisk disk, Map disksMapping, Backup backup) { + if (!disksMapping.containsKey(disk)) { + return false; + } + VolumeVO volumeVO = disksMapping.get(disk); + if (volumeVO == null) { + final VMInstanceVO vm = _vmDao.findByIdIncludingRemoved(backup.getVmId()); + if (vm == null) { + throw new CloudRuntimeException("Failed to find the volumes details from the VM backup"); + } + List backedUpVolumes = vm.getBackupVolumeList(); + for (Backup.VolumeInfo backedUpVolume : backedUpVolumes) { + if (backedUpVolume.getSize().equals(disk.getCapacityInBytes())) { + return backedUpVolume.getType().equals(Volume.Type.ROOT); + } + } + } else { + return volumeVO.getVolumeType().equals(Volume.Type.ROOT); + } + throw new CloudRuntimeException("Could not determinate ROOT disk for VM to import"); + } + + /** + * Check backing info + */ + private void checkBackingInfo(VirtualDeviceBackingInfo backingInfo) { + if (! (backingInfo instanceof VirtualDiskFlatVer2BackingInfo)) { + throw new CloudRuntimeException("Unsopported backing, expected " + VirtualDiskFlatVer2BackingInfo.class.getSimpleName()); + } + } + + /** + * Get pool ID from datastore UUID + */ + private Long getPoolIdFromDatastoreUuid(String datastoreUuid) { + String poolUuid = UuidUtils.normalize(datastoreUuid); + StoragePoolVO pool = _storagePoolDao.findByUuid(poolUuid); + if (pool == null) { + throw new CloudRuntimeException("Couldn't find storage pool " + poolUuid); + } + return pool.getId(); + } + + /** + * Get pool ID for disk + */ + private Long getPoolId(VirtualDisk disk) { + VirtualDeviceBackingInfo backing = disk.getBacking(); + checkBackingInfo(backing); + VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo) backing; + String[] fileNameParts = info.getFileName().split(" "); + String datastoreUuid = StringUtils.substringBetween(fileNameParts[0], "[", "]"); + return getPoolIdFromDatastoreUuid(datastoreUuid); + } + + /** + * Get volume name from filename + */ + private String getVolumeNameFromFileName(String fileName) { + String[] fileNameParts = fileName.split(" "); + String volumePath = fileNameParts[1]; + return volumePath.split("/")[1].replaceFirst(".vmdk", ""); + } + + /** + * Get root disk template path + */ + private String getRootDiskTemplatePath(VirtualDisk rootDisk) { + VirtualDeviceBackingInfo backing = rootDisk.getBacking(); + checkBackingInfo(backing); + VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo) backing; + VirtualDiskFlatVer2BackingInfo parent = info.getParent(); + return (parent != null) ? getVolumeNameFromFileName(parent.getFileName()) : getVolumeNameFromFileName(info.getFileName()); + } + + /** + * Get template MO + */ + private VirtualMachineMO getTemplate(DatacenterMO dcMo, String templatePath) throws Exception { + VirtualMachineMO template = dcMo.findVm(templatePath); + if (!template.isTemplate()) { + throw new CloudRuntimeException(templatePath + " is not a template"); + } + return template; + } + + /** + * Get template pool ID + */ + private Long getTemplatePoolId(VirtualMachineMO template) throws Exception { + VirtualMachineConfigSummary configSummary = template.getConfigSummary(); + String vmPathName = configSummary.getVmPathName(); + String[] pathParts = vmPathName.split(" "); + String dataStoreUuid = pathParts[0].replace("[", "").replace("]", ""); + return getPoolIdFromDatastoreUuid(dataStoreUuid); + } + + /** + * Get template size + */ + private Long getTemplateSize(VirtualMachineMO template, String vmInternalName, + Map disksMapping, Backup backup) throws Exception { + List disks = template.getVirtualDisks(); + if (CollectionUtils.isEmpty(disks)) { + throw new CloudRuntimeException("Couldn't find VM template size"); + } + return disks.get(0).getCapacityInBytes(); + } + + /** + * Create a VM Template record on DB + */ + private VMTemplateVO createVMTemplateRecord(String vmInternalName, long guestOsId, long accountId) { + Long nextTemplateId = vmTemplateDao.getNextInSequence(Long.class, "id"); + VMTemplateVO templateVO = new VMTemplateVO(nextTemplateId, "Imported-from-" + vmInternalName, + Storage.ImageFormat.OVA,false, false, false, Storage.TemplateType.USER, + null, false, 64, accountId, null, "Template imported from VM " + vmInternalName, + false, guestOsId, false, HypervisorType.VMware, null, null, + false, false, false); + return vmTemplateDao.persist(templateVO); + } + + /** + * Retrieve the template ID matching the template on templatePath. There are 2 cases: + * - There are no references on DB for primary storage -> create a template DB record and return its ID + * - There are references on DB for primary storage -> return template ID for any of those references + */ + private long getTemplateId(String templatePath, String vmInternalName, Long guestOsId, long accountId) { + List poolRefs = templateStoragePoolDao.listByTemplatePath(templatePath); + return CollectionUtils.isNotEmpty(poolRefs) ? + poolRefs.get(0).getTemplateId() : + createVMTemplateRecord(vmInternalName, guestOsId, accountId).getId(); + } + + /** + * Update template reference on primary storage, if needed + */ + private void updateTemplateRef(long templateId, Long poolId, String templatePath, Long templateSize) { + VMTemplateStoragePoolVO templateRef = templateStoragePoolDao.findByPoolPath(poolId, templatePath); + if (templateRef == null) { + templateRef = new VMTemplateStoragePoolVO(poolId, templateId, null, 100, + VMTemplateStorageResourceAssoc.Status.DOWNLOADED, templatePath, null, + null, templatePath, templateSize); + templateRef.setState(ObjectInDataStoreStateMachine.State.Ready); + templateStoragePoolDao.persist(templateRef); + } + } + + /** + * Get template ID for VM being imported. If it is not found, it is created + */ + private Long getImportingVMTemplate(List virtualDisks, DatacenterMO dcMo, String vmInternalName, + Long guestOsId, long accountId, Map disksMapping, Backup backup) throws Exception { + for (VirtualDisk disk : virtualDisks) { + if (isRootDisk(disk, disksMapping, backup)) { + VolumeVO volumeVO = disksMapping.get(disk); + if (volumeVO == null) { + String templatePath = getRootDiskTemplatePath(disk); + VirtualMachineMO template = getTemplate(dcMo, templatePath); + Long poolId = getTemplatePoolId(template); + Long templateSize = getTemplateSize(template, vmInternalName, disksMapping, backup); + long templateId = getTemplateId(templatePath, vmInternalName, guestOsId, accountId); + updateTemplateRef(templateId, poolId, templatePath, templateSize); + return templateId; + } else { + return volumeVO.getTemplateId(); + } + } + } + throw new CloudRuntimeException("Could not find ROOT disk for VM " + vmInternalName); + } + + /** + * If VM does not exist: create and persist VM + * If VM exists: update VM + */ + private VMInstanceVO getVM(String vmInternalName, long templateId, long guestOsId, + long serviceOfferingId, long zoneId, long accountId, long userId, + long domainId) { + VMInstanceVO vm = _vmDao.findVMByInstanceNameIncludingRemoved(vmInternalName); + if (vm != null) { + vm.setState(VirtualMachine.State.Stopped); + vm.setPowerState(VirtualMachine.PowerState.PowerOff); + _vmDao.update(vm.getId(), vm); + if (vm.getRemoved() != null) { + _vmDao.unremove(vm.getId()); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_CREATE, accountId, vm.getDataCenterId(), vm.getId(), + vm.getHostName(), vm.getServiceOfferingId(), vm.getTemplateId(), + vm.getHypervisorType().toString(), VirtualMachine.class.getName(), vm.getUuid(), vm.isDisplayVm()); + } + return _vmDao.findById(vm.getId()); + } else { + long id = userVmDao.getNextInSequence(Long.class, "id"); + UserVmVO vmInstanceVO = new UserVmVO(id, vmInternalName, vmInternalName, templateId, HypervisorType.VMware, + guestOsId, false, false, domainId, accountId, userId, serviceOfferingId, + null, vmInternalName, null); + vmInstanceVO.setDataCenterId(zoneId); + return userVmDao.persist(vmInstanceVO); + } + } + + /** + * Create and persist volume + */ + private VolumeVO createVolumeRecord(Volume.Type type, String volumeName, long zoneId, long domainId, + long accountId, long diskOfferingId, Storage.ProvisioningType provisioningType, + Long size, long instanceId, Long poolId, long templateId, Integer unitNumber, VirtualMachineDiskInfo diskInfo) { + VolumeVO volumeVO = new VolumeVO(type, volumeName, zoneId, domainId, accountId, diskOfferingId, + provisioningType, size, null, null, null); + volumeVO.setFormat(Storage.ImageFormat.OVA); + volumeVO.setPath(volumeName); + volumeVO.setState(Volume.State.Ready); + volumeVO.setInstanceId(instanceId); + volumeVO.setPoolId(poolId); + volumeVO.setTemplateId(templateId); + volumeVO.setAttached(new Date()); + volumeVO.setRemoved(null); + volumeVO.setChainInfo(new Gson().toJson(diskInfo)); + if (unitNumber != null) { + volumeVO.setDeviceId(unitNumber.longValue()); + } + return _volumeDao.persist(volumeVO); + } + + /** + * Get volume name from VM disk + */ + private String getVolumeName(VirtualDisk disk, VirtualMachineMO vmToImport) throws Exception { + return vmToImport.getVmdkFileBaseName(disk); + } + + /** + * Get provisioning type for VM disk info + */ + private Storage.ProvisioningType getProvisioningType(VirtualDiskFlatVer2BackingInfo backing) { + Boolean thinProvisioned = backing.isThinProvisioned(); + if (BooleanUtils.isTrue(thinProvisioned)) { + return Storage.ProvisioningType.THIN; + } + return Storage.ProvisioningType.SPARSE; + } + + /** + * Get disk offering ID for volume being imported. If it is not found it is mapped to "Custom" ID + */ + private long getDiskOfferingId(long size, Storage.ProvisioningType provisioningType) { + List offerings = diskOfferingDao.listAllBySizeAndProvisioningType(size, provisioningType); + return CollectionUtils.isNotEmpty(offerings) ? + offerings.get(0).getId() : + diskOfferingDao.findByUniqueName("Cloud.Com-Custom").getId(); + } + + protected VolumeVO updateVolume(VirtualDisk disk, Map disksMapping, VirtualMachineMO vmToImport, Long poolId, VirtualMachine vm) throws Exception { + VolumeVO volume = disksMapping.get(disk); + String volumeName = getVolumeName(disk, vmToImport); + volume.setPath(volumeName); + volume.setPoolId(poolId); + VirtualMachineDiskInfo diskInfo = getDiskInfo(vmToImport, poolId, volumeName); + volume.setChainInfo(new Gson().toJson(diskInfo)); + volume.setInstanceId(vm.getId()); + volume.setState(Volume.State.Ready); + volume.setAttached(new Date()); + _volumeDao.update(volume.getId(), volume); + if (volume.getRemoved() != null) { + _volumeDao.unremove(volume.getId()); + if (vm.getType() == Type.User) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_CREATE, volume.getAccountId(), volume.getDataCenterId(), + volume.getId(), volume.getName(), volume.getDiskOfferingId(), null, volume.getSize(), + Volume.class.getName(), volume.getUuid(), volume.isDisplayVolume()); + _resourceLimitService.incrementResourceCount(vm.getAccountId(), Resource.ResourceType.volume, volume.isDisplayVolume()); + _resourceLimitService.incrementResourceCount(vm.getAccountId(), Resource.ResourceType.primary_storage, volume.isDisplayVolume(), volume.getSize()); + } + } + return volume; + } + + /** + * Get volumes for VM being imported + */ + private void syncVMVolumes(VMInstanceVO vmInstanceVO, List virtualDisks, + Map disksMapping, VirtualMachineMO vmToImport, Backup backup) throws Exception { + long zoneId = vmInstanceVO.getDataCenterId(); + long accountId = vmInstanceVO.getAccountId(); + long domainId = vmInstanceVO.getDomainId(); + long templateId = vmInstanceVO.getTemplateId(); + long instanceId = vmInstanceVO.getId(); + + for (VirtualDisk disk : virtualDisks) { + Long poolId = getPoolId(disk); + Volume volume = null; + if (disksMapping.containsKey(disk) && disksMapping.get(disk) != null) { + volume = updateVolume(disk, disksMapping, vmToImport, poolId, vmInstanceVO); + } else { + volume = createVolume(disk, vmToImport, domainId, zoneId, accountId, instanceId, poolId, templateId, backup, true); + } + s_logger.debug("VM backup restored (updated/created) volume id:" + volume.getId() + " for VM id:" + instanceId); + } + } + + private VirtualMachineDiskInfo getDiskInfo(VirtualMachineMO vmMo, Long poolId, String volumeName) throws Exception { + VirtualMachineDiskInfoBuilder diskInfoBuilder = vmMo.getDiskInfoBuilder(); + String poolName = _storagePoolDao.findById(poolId).getUuid().replace("-", ""); + return diskInfoBuilder.getDiskInfoByBackingFileBaseName(volumeName, poolName); + } + + private VolumeVO createVolume(VirtualDisk disk, VirtualMachineMO vmToImport, long domainId, long zoneId, + long accountId, long instanceId, Long poolId, long templateId, Backup backup, boolean isImport) throws Exception { + VMInstanceVO vm = _vmDao.findByIdIncludingRemoved(backup.getVmId()); + if (vm == null) { + throw new CloudRuntimeException("Failed to find the backup volume information from the VM backup"); + } + List backedUpVolumes = vm.getBackupVolumeList(); + Volume.Type type = Volume.Type.DATADISK; + Long size = disk.getCapacityInBytes(); + if (isImport) { + for (Backup.VolumeInfo volumeInfo : backedUpVolumes) { + if (volumeInfo.getSize().equals(disk.getCapacityInBytes())) { + type = volumeInfo.getType(); + } + } + } + VirtualDeviceBackingInfo backing = disk.getBacking(); + checkBackingInfo(backing); + VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo) backing; + String volumeName = getVolumeName(disk, vmToImport); + Storage.ProvisioningType provisioningType = getProvisioningType(info); + long diskOfferingId = getDiskOfferingId(size, provisioningType); + Integer unitNumber = disk.getUnitNumber(); + VirtualMachineDiskInfo diskInfo = getDiskInfo(vmToImport, poolId, volumeName); + return createVolumeRecord(type, volumeName, zoneId, domainId, accountId, diskOfferingId, + provisioningType, size, instanceId, poolId, templateId, unitNumber, diskInfo); + } + + /** + * Get physical network ID from zoneId and Vmware label + */ + private long getPhysicalNetworkId(Long zoneId, String tag) { + List physicalNetworks = physicalNetworkDao.listByZone(zoneId); + for (PhysicalNetworkVO physicalNetwork : physicalNetworks) { + PhysicalNetworkTrafficTypeVO vo = _physicalNetworkTrafficTypeDao.findBy(physicalNetwork.getId(), TrafficType.Guest); + if (vo == null) { + continue; + } + String vmwareNetworkLabel = vo.getVmwareNetworkLabel(); + if (!vmwareNetworkLabel.startsWith(tag)) { + throw new CloudRuntimeException("Vmware network label does not start with: " + tag); + } + return physicalNetwork.getId(); + } + throw new CloudRuntimeException("Could not find guest physical network matching tag: " + tag + " on zone " + zoneId); + } + + /** + * Create and persist network + */ + private NetworkVO createNetworkRecord(Long zoneId, String tag, String vlan, long accountId, long domainId) { + Long physicalNetworkId = getPhysicalNetworkId(zoneId, tag); + final long id = _networkDao.getNextInSequence(Long.class, "id"); + NetworkVO networkVO = new NetworkVO(id, TrafficType.Guest, Networks.Mode.Dhcp, BroadcastDomainType.Vlan, 9L, + domainId, accountId, id, "Imported-network-" + id, "Imported-network-" + id, null, Network.GuestType.Isolated, + zoneId, physicalNetworkId, ControlledEntity.ACLType.Account, false, null, false); + networkVO.setBroadcastUri(BroadcastDomainType.Vlan.toUri(vlan)); + networkVO.setGuruName("ExternalGuestNetworkGuru"); + networkVO.setState(Network.State.Implemented); + return _networkDao.persist(networkVO); + } + + /** + * Get network from VM network name + */ + private NetworkVO getGuestNetworkFromNetworkMorName(String name, long accountId, Long zoneId, long domainId) { + String prefix = "cloud.guest."; + String nameWithoutPrefix = name.replace(prefix, ""); + String[] parts = nameWithoutPrefix.split("\\."); + String vlan = parts[0]; + String tag = parts[parts.length - 1]; + String[] tagSplit = tag.split("-"); + tag = tagSplit[tagSplit.length - 1]; + NetworkVO networkVO = _networkDao.findByVlan(vlan); + if (networkVO == null) { + networkVO = createNetworkRecord(zoneId, tag, vlan, accountId, domainId); + } + return networkVO; + } + + /** + * Get map between VM networks and its IDs on CloudStack + */ + private Map getNetworksMapping(String[] vmNetworkNames, long accountId, long zoneId, long domainId) { + Map mapping = new HashMap<>(); + for (String networkName : vmNetworkNames) { + NetworkVO networkVO = getGuestNetworkFromNetworkMorName(networkName, accountId, zoneId, domainId); + mapping.put(networkName, networkVO); + } + return mapping; + } + + /** + * Get network MO from VM NIC + */ + private NetworkMO getNetworkMO(VirtualE1000 nic, VmwareContext context) { + VirtualDeviceConnectInfo connectable = nic.getConnectable(); + VirtualEthernetCardNetworkBackingInfo info = (VirtualEthernetCardNetworkBackingInfo) nic.getBacking(); + ManagedObjectReference networkMor = info.getNetwork(); + if (networkMor == null) { + throw new CloudRuntimeException("Could not find network for NIC on: " + nic.getMacAddress()); + } + return new NetworkMO(context, networkMor); + } + + private Pair getNicMacAddressAndNetworkName(VirtualDevice nicDevice, VmwareContext context) throws Exception { + VirtualE1000 nic = (VirtualE1000) nicDevice; + String macAddress = nic.getMacAddress(); + NetworkMO networkMO = getNetworkMO(nic, context); + String networkName = networkMO.getName(); + return new Pair<>(macAddress, networkName); + } + + private void syncVMNics(VirtualDevice[] nicDevices, DatacenterMO dcMo, Map networksMapping, + VMInstanceVO vm) throws Exception { + VmwareContext context = dcMo.getContext(); + List allNics = _nicDao.listByVmId(vm.getId()); + for (VirtualDevice nicDevice : nicDevices) { + Pair pair = getNicMacAddressAndNetworkName(nicDevice, context); + String macAddress = pair.first(); + String networkName = pair.second(); + NetworkVO networkVO = networksMapping.get(networkName); + NicVO nicVO = _nicDao.findByNetworkIdAndMacAddress(networkVO.getId(), macAddress); + if (nicVO != null) { + allNics.remove(nicVO); + } + } + for (final NicVO unMappedNic : allNics) { + vmManager.removeNicFromVm(vm, unMappedNic); + } + } + + private Map getDisksMapping(Backup backup, List virtualDisks) { + final VMInstanceVO vm = _vmDao.findByIdIncludingRemoved(backup.getVmId()); + if (vm == null) { + throw new CloudRuntimeException("Failed to find the volumes details from the VM backup"); + } + List backedUpVolumes = vm.getBackupVolumeList(); + Map usedVols = new HashMap<>(); + Map map = new HashMap<>(); + + for (Backup.VolumeInfo backedUpVol : backedUpVolumes) { + for (VirtualDisk disk : virtualDisks) { + if (!map.containsKey(disk) && backedUpVol.getSize().equals(disk.getCapacityInBytes()) + && !usedVols.containsKey(backedUpVol.getUuid())) { + String volId = backedUpVol.getUuid(); + VolumeVO vol = _volumeDao.findByUuidIncludingRemoved(volId); + usedVols.put(backedUpVol.getUuid(), true); + map.put(disk, vol); + s_logger.debug("VM restore mapping for disk " + disk.getBacking() + + " (capacity: " + disk.getCapacityInBytes() + ") with volume ID" + vol.getId()); + } + } + } + return map; + } + + /** + * Find VM on datacenter + */ + private VirtualMachineMO findVM(DatacenterMO dcMo, String path) throws Exception { + VirtualMachineMO vm = dcMo.findVm(path); + if (vm == null) { + throw new CloudRuntimeException("Error finding VM: " + path); + } + return vm; + } + + /** + * Find restored volume based on volume info + */ + private VirtualDisk findRestoredVolume(Backup.VolumeInfo volumeInfo, VirtualMachineMO vm) throws Exception { + List virtualDisks = vm.getVirtualDisks(); + for (VirtualDisk disk: virtualDisks) { + if (disk.getCapacityInBytes().equals(volumeInfo.getSize())) { + return disk; + } + } + throw new CloudRuntimeException("Volume to restore could not be found"); + } + + /** + * Get volume full path + */ + private String getVolumeFullPath(VirtualDisk disk) { + VirtualDeviceBackingInfo backing = disk.getBacking(); + checkBackingInfo(backing); + VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo) backing; + return info.getFileName(); + } + + /** + * Get dest volume full path + */ + private String getDestVolumeFullPath(VirtualDisk restoredDisk, VirtualMachineMO restoredVm, + VirtualMachineMO vmMo) throws Exception { + VirtualDisk vmDisk = vmMo.getVirtualDisks().get(0); + String vmDiskPath = vmMo.getVmdkFileBaseName(vmDisk); + String vmDiskFullPath = getVolumeFullPath(vmMo.getVirtualDisks().get(0)); + String restoredVolumePath = restoredVm.getVmdkFileBaseName(restoredDisk); + return vmDiskFullPath.replace(vmDiskPath, restoredVolumePath); + } + + /** + * Get dest datastore mor + */ + private ManagedObjectReference getDestStoreMor(VirtualMachineMO vmMo) throws Exception { + VirtualDisk vmDisk = vmMo.getVirtualDisks().get(0); + VirtualDeviceBackingInfo backing = vmDisk.getBacking(); + checkBackingInfo(backing); + VirtualDiskFlatVer2BackingInfo info = (VirtualDiskFlatVer2BackingInfo) backing; + return info.getDatastore(); + } + + @Override + public VirtualMachine importVirtualMachineFromBackup(long zoneId, long domainId, long accountId, long userId, + String vmInternalName, Backup backup) throws Exception { + DatacenterMO dcMo = getDatacenterMO(zoneId); + VirtualMachineMO vmToImport = dcMo.findVm(vmInternalName); + if (vmToImport == null) { + throw new CloudRuntimeException("Error finding VM: " + vmInternalName); + } + VirtualMachineConfigSummary configSummary = vmToImport.getConfigSummary(); + VirtualMachineRuntimeInfo runtimeInfo = vmToImport.getRuntimeInfo(); + List virtualDisks = vmToImport.getVirtualDisks(); + String[] vmNetworkNames = vmToImport.getNetworks(); + VirtualDevice[] nicDevices = vmToImport.getNicDevices(); + + Map disksMapping = getDisksMapping(backup, virtualDisks); + Map networksMapping = getNetworksMapping(vmNetworkNames, accountId, zoneId, domainId); + + long guestOsId = getImportingVMGuestOs(configSummary); + long serviceOfferingId = getImportingVMServiceOffering(configSummary, runtimeInfo); + long templateId = getImportingVMTemplate(virtualDisks, dcMo, vmInternalName, guestOsId, accountId, disksMapping, backup); + + VMInstanceVO vm = getVM(vmInternalName, templateId, guestOsId, serviceOfferingId, zoneId, accountId, userId, domainId); + syncVMVolumes(vm, virtualDisks, disksMapping, vmToImport, backup); + syncVMNics(nicDevices, dcMo, networksMapping, vm); + + return vm; + } + + @Override + public boolean attachRestoredVolumeToVirtualMachine(long zoneId, String location, Backup.VolumeInfo volumeInfo, + VirtualMachine vm, long poolId, Backup backup) throws Exception { + DatacenterMO dcMo = getDatacenterMO(zoneId); + VirtualMachineMO vmRestored = findVM(dcMo, location); + VirtualMachineMO vmMo = findVM(dcMo, vm.getInstanceName()); + VirtualDisk restoredDisk = findRestoredVolume(volumeInfo, vmRestored); + String diskPath = vmRestored.getVmdkFileBaseName(restoredDisk); + + s_logger.debug("Restored disk size=" + restoredDisk.getCapacityInKB() + " path=" + diskPath); + + // Detach restored VM disks + vmRestored.detachAllDisks(); + + String srcPath = getVolumeFullPath(restoredDisk); + String destPath = getDestVolumeFullPath(restoredDisk, vmRestored, vmMo); + + VirtualDiskManagerMO virtualDiskManagerMO = new VirtualDiskManagerMO(dcMo.getContext()); + + // Copy volume to the VM folder + virtualDiskManagerMO.moveVirtualDisk(srcPath, dcMo.getMor(), destPath, dcMo.getMor(), true); + + try { + // Attach volume to VM + vmMo.attachDisk(new String[] {destPath}, getDestStoreMor(vmMo)); + } catch (Exception e) { + s_logger.error("Failed to attach the restored volume: " + diskPath, e); + return false; + } finally { + // Destroy restored VM + vmRestored.destroy(); + } + + VirtualDisk attachedDisk = getAttachedDisk(vmMo, diskPath); + if (attachedDisk == null) { + s_logger.error("Failed to get the attached the (restored) volume " + diskPath); + return false; + } + createVolume(attachedDisk, vmMo, vm.getDomainId(), vm.getDataCenterId(), vm.getAccountId(), vm.getId(), + poolId, vm.getTemplateId(), backup, false); + + return true; + } + + private VirtualDisk getAttachedDisk(VirtualMachineMO vmMo, String diskPath) throws Exception { + for (VirtualDisk disk : vmMo.getVirtualDisks()) { + if (vmMo.getVmdkFileBaseName(disk).equals(diskPath)) { + return disk; + } + } + return null; + } + @Override public List finalizeMigrate(VirtualMachine vm, StoragePool destination) { List commands = new ArrayList(); diff --git a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java index 1d3c1ad9d69..c4b939a1149 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareManagerImpl.java @@ -199,7 +199,7 @@ public class VmwareManagerImpl extends ManagerBase implements VmwareManager, Vmw @Inject private PrimaryDataStoreDao primaryStorageDao; @Inject - private VMTemplatePoolDao templateDataStoreDao; + private VMTemplatePoolDao templateStoragePoolDao; @Inject private TemplateJoinDao templateDao; @Inject @@ -1443,7 +1443,7 @@ public class VmwareManagerImpl extends ManagerBase implements VmwareManager, Vmw */ private Runnable getCleanupFullyClonedTemplatesTask() { return new CleanupFullyClonedTemplatesTask(primaryStorageDao, - templateDataStoreDao, + templateStoragePoolDao, templateDao, vmInstanceDao, cloneSettingDao, diff --git a/plugins/pom.xml b/plugins/pom.xml index 4a96b4d2a43..2ecd6972211 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -58,6 +58,8 @@ api/rate-limit api/solidfire-intg-test + backup/dummy + ca/root-ca database/quota @@ -187,21 +189,12 @@ + api/vmware-sioc + backup/veeam hypervisors/vmware network-elements/cisco-vnmc - - vmware-sioc - - - noredist - - - - api/vmware-sioc - - mysqlha diff --git a/pom.xml b/pom.xml index 075b9562d35..32d925fe4ac 100644 --- a/pom.xml +++ b/pom.xml @@ -121,7 +121,7 @@ 1.64 3.3.0 8.18 - 3.2.0 + 3.2.6 2.6.11 0.0.27 2.4.17 @@ -130,7 +130,7 @@ 4.5.11 4.4.13 2.17 - 2.9.2 + 2.9.6 1.9.3 0.17 3.26.0-GA @@ -161,6 +161,7 @@ 8.5.47 1.0.0-build222 6.7 + 0.5.0 6.2.0-3.1 3.1.3 1.4.11.1 diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index 74579813c3d..8d476ba78dd 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -39,6 +39,9 @@ import org.apache.cloudstack.api.ApiConstants.VMDetails; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.AsyncJobResponse; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.BackupScheduleResponse; import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.DomainRouterResponse; @@ -63,6 +66,12 @@ import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.VpcOfferingResponse; import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupSchedule; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupScheduleDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; @@ -446,6 +455,9 @@ public class ApiDBUtils { static ResourceMetaDataService s_resourceDetailsService; static HostGpuGroupsDao s_hostGpuGroupsDao; static VGPUTypesDao s_vgpuTypesDao; + static BackupDao s_backupDao; + static BackupScheduleDao s_backupScheduleDao; + static BackupOfferingDao s_backupOfferingDao; @Inject private ManagementServer ms; @@ -684,6 +696,12 @@ public class ApiDBUtils { private HostGpuGroupsDao hostGpuGroupsDao; @Inject private VGPUTypesDao vgpuTypesDao; + @Inject + private BackupDao backupDao; + @Inject + private BackupOfferingDao backupOfferingDao; + @Inject + private BackupScheduleDao backupScheduleDao; @PostConstruct void init() { @@ -806,6 +824,9 @@ public class ApiDBUtils { s_resourceDetailsService = resourceDetailsService; s_hostGpuGroupsDao = hostGpuGroupsDao; s_vgpuTypesDao = vgpuTypesDao; + s_backupDao = backupDao; + s_backupScheduleDao = backupScheduleDao; + s_backupOfferingDao = backupOfferingDao; } // /////////////////////////////////////////////////////////// @@ -2037,4 +2058,16 @@ public class ApiDBUtils { public static List listResourceTagViewByResourceUUID(String resourceUUID, ResourceObjectType resourceType) { return s_tagJoinDao.listBy(resourceUUID, resourceType); } + + public static BackupResponse newBackupResponse(Backup backup) { + return s_backupDao.newBackupResponse(backup); + } + + public static BackupScheduleResponse newBackupScheduleResponse(BackupSchedule schedule) { + return s_backupScheduleDao.newBackupScheduleResponse(schedule); + } + + public static BackupOfferingResponse newBackupOfferingResponse(BackupOffering policy) { + return s_backupOfferingDao.newBackupOfferingResponse(policy); + } } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index c6744074ae3..9da3ae4f710 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -49,6 +49,9 @@ import org.apache.cloudstack.api.response.AsyncJobResponse; import org.apache.cloudstack.api.response.AutoScalePolicyResponse; import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse; import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.BackupResponse; +import org.apache.cloudstack.api.response.BackupScheduleResponse; import org.apache.cloudstack.api.response.CapabilityResponse; import org.apache.cloudstack.api.response.CapacityResponse; import org.apache.cloudstack.api.response.ClusterResponse; @@ -141,6 +144,10 @@ import org.apache.cloudstack.api.response.VpcOfferingResponse; import org.apache.cloudstack.api.response.VpcResponse; import org.apache.cloudstack.api.response.VpnUsersResponse; import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.BackupSchedule; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; @@ -378,6 +385,8 @@ public class ApiResponseHelper implements ResponseGenerator { NetworkDetailsDao networkDetailsDao; @Inject private VMSnapshotDao vmSnapshotDao; + @Inject + private BackupOfferingDao backupOfferingDao; @Override public UserResponse createUserResponse(User user) { @@ -3660,6 +3669,25 @@ public class ApiResponseHelper implements ResponseGenerator { } usageRecResponse.setDescription(builder.toString()); } + } else if (usageRecord.getUsageType() == UsageTypes.BACKUP) { + resourceType = ResourceObjectType.Backup; + final StringBuilder builder = new StringBuilder(); + builder.append("Backup usage of size ").append(usageRecord.getUsageDisplay()); + if (vmInstance != null) { + resourceId = vmInstance.getId(); + usageRecResponse.setResourceName(vmInstance.getInstanceName()); + usageRecResponse.setUsageId(vmInstance.getUuid()); + builder.append(" for VM ").append(vmInstance.getHostName()) + .append(" (").append(vmInstance.getUuid()).append(")"); + final BackupOffering backupOffering = backupOfferingDao.findByIdIncludingRemoved(usageRecord.getOfferingId()); + if (backupOffering != null) { + builder.append(" and backup offering ").append(backupOffering.getName()) + .append(" (").append(backupOffering.getUuid()).append(", user ad-hoc/scheduled backup allowed: ") + .append(backupOffering.isUserDrivenBackupAllowed()).append(")"); + } + + } + usageRecResponse.setDescription(builder.toString()); } else if (usageRecord.getUsageType() == UsageTypes.VM_SNAPSHOT) { resourceType = ResourceObjectType.VMSnapshot; VMSnapshotVO vmSnapshotVO = null; @@ -3672,6 +3700,9 @@ public class ApiResponseHelper implements ResponseGenerator { } } usageRecResponse.setSize(usageRecord.getSize()); + if (usageRecord.getVirtualSize() != null) { + usageRecResponse.setVirtualSize(usageRecord.getVirtualSize()); + } if (usageRecord.getOfferingId() != null) { usageRecResponse.setOfferingId(usageRecord.getOfferingId().toString()); } @@ -4210,6 +4241,22 @@ public class ApiResponseHelper implements ResponseGenerator { response.setDomainName(domain.getName()); return response; } + + @Override + public BackupResponse createBackupResponse(Backup backup) { + return ApiDBUtils.newBackupResponse(backup); + } + + @Override + public BackupScheduleResponse createBackupScheduleResponse(BackupSchedule schedule) { + return ApiDBUtils.newBackupScheduleResponse(schedule); + } + + @Override + public BackupOfferingResponse createBackupOfferingResponse(BackupOffering policy) { + return ApiDBUtils.newBackupOfferingResponse(policy); + } + public ManagementServerResponse createManagementResponse(ManagementServerHost mgmt) { ManagementServerResponse response = new ManagementServerResponse(); response.setId(mgmt.getUuid()); diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 4ccfce9edb4..a612829507e 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -28,7 +28,6 @@ import java.util.stream.Collectors; import javax.inject.Inject; -import com.cloud.vm.UserVmManager; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants.VMDetails; @@ -57,6 +56,7 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.net.Dhcp; import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.UserVmManager; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VmStats; import com.cloud.vm.dao.NicExtraDhcpOptionDao; @@ -174,6 +174,10 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation finalizeMigrate(VirtualMachine vm, StoragePool destination) { return null; } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 15e32208b0f..88af6e9aa85 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -1637,6 +1637,11 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic throw new InvalidParameterValueException("Unable to attach volume, please specify a VM that does not have VM snapshots"); } + // if target VM has backups + if (vm.getBackupOfferingId() != null || vm.getBackupVolumeList().size() > 0) { + throw new InvalidParameterValueException("Unable to attach volume, please specify a VM that does not have any backups"); + } + // permission check _accountMgr.checkAccess(caller, null, true, volumeToAttach, vm); @@ -1877,6 +1882,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic throw new InvalidParameterValueException("Unable to detach volume, please specify a VM that does not have VM snapshots"); } + if (vm.getBackupOfferingId() != null || vm.getBackupVolumeList().size() > 0) { + throw new InvalidParameterValueException("Unable to detach volume, cannot detach volume from a VM that has backups. First remove the VM from the backup offering."); + } + AsyncJobExecutionContext asyncExecutionContext = AsyncJobExecutionContext.getCurrentExecutionContext(); if (asyncExecutionContext != null) { AsyncJob job = asyncExecutionContext.getJob(); diff --git a/server/src/main/java/com/cloud/usage/UsageServiceImpl.java b/server/src/main/java/com/cloud/usage/UsageServiceImpl.java index 531b2917043..4f09d13f3cc 100644 --- a/server/src/main/java/com/cloud/usage/UsageServiceImpl.java +++ b/server/src/main/java/com/cloud/usage/UsageServiceImpl.java @@ -16,6 +16,28 @@ // under the License. package com.cloud.usage; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.command.admin.usage.GenerateUsageRecordsCmd; +import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd; +import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd; +import org.apache.cloudstack.api.response.UsageTypeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.usage.Usage; +import org.apache.cloudstack.usage.UsageService; +import org.apache.cloudstack.usage.UsageTypes; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + import com.cloud.configuration.Config; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; @@ -55,26 +77,6 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.TransactionLegacy; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.VMInstanceDao; -import org.apache.cloudstack.api.command.admin.usage.GenerateUsageRecordsCmd; -import org.apache.cloudstack.api.command.admin.usage.ListUsageRecordsCmd; -import org.apache.cloudstack.api.command.admin.usage.RemoveRawUsageRecordsCmd; -import org.apache.cloudstack.api.response.UsageTypeResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.usage.Usage; -import org.apache.cloudstack.usage.UsageService; -import org.apache.cloudstack.usage.UsageTypes; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.TimeZone; @Component public class UsageServiceImpl extends ManagerBase implements UsageService, Manager { @@ -267,6 +269,7 @@ public class UsageServiceImpl extends ManagerBase implements UsageService, Manag case UsageTypes.RUNNING_VM: case UsageTypes.ALLOCATED_VM: case UsageTypes.VM_SNAPSHOT: + case UsageTypes.BACKUP: VMInstanceVO vm = _vmDao.findByUuidIncludingRemoved(usageId); if (vm != null) { usageDbId = vm.getId(); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java new file mode 100644 index 00000000000..c91805dd4dc --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -0,0 +1,1063 @@ +// 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.backup; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.Timer; +import java.util.TimerTask; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.admin.backup.DeleteBackupOfferingCmd; +import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; +import org.apache.cloudstack.api.command.admin.backup.ListBackupProviderOfferingsCmd; +import org.apache.cloudstack.api.command.admin.backup.ListBackupProvidersCmd; +import org.apache.cloudstack.api.command.user.backup.AssignVirtualMachineToBackupOfferingCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; +import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.DeleteBackupCmd; +import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; +import org.apache.cloudstack.api.command.user.backup.RemoveVirtualMachineFromBackupOfferingCmd; +import org.apache.cloudstack.api.command.user.backup.RestoreBackupCmd; +import org.apache.cloudstack.api.command.user.backup.RestoreVolumeFromBackupAndAttachToVMCmd; +import org.apache.cloudstack.api.command.user.backup.UpdateBackupScheduleCmd; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupScheduleDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; +import org.apache.cloudstack.framework.jobs.AsyncJobManager; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.cloudstack.poll.BackgroundPollManager; +import org.apache.cloudstack.poll.BackgroundPollTask; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.log4j.Logger; + +import com.cloud.api.ApiDispatcher; +import com.cloud.api.ApiGsonHelper; +import com.cloud.dc.DataCenter; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.ActionEventUtils; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.projects.Project; +import com.cloud.storage.ScopeType; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.usage.dao.UsageBackupDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountService; +import com.cloud.user.User; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.ComponentContext; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.db.JoinBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionStatus; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; +import com.google.common.base.Strings; +import com.google.gson.Gson; + +public class BackupManagerImpl extends ManagerBase implements BackupManager { + private static final Logger LOG = Logger.getLogger(BackupManagerImpl.class); + + @Inject + private BackupDao backupDao; + @Inject + private BackupScheduleDao backupScheduleDao; + @Inject + private BackupOfferingDao backupOfferingDao; + @Inject + private VMInstanceDao vmInstanceDao; + @Inject + private AccountService accountService; + @Inject + private AccountManager accountManager; + @Inject + private UsageBackupDao usageBackupDao; + @Inject + private VolumeDao volumeDao; + @Inject + private DataCenterDao dataCenterDao; + @Inject + private BackgroundPollManager backgroundPollManager; + @Inject + private HostDao hostDao; + @Inject + private HypervisorGuruManager hypervisorGuruManager; + @Inject + private PrimaryDataStoreDao primaryDataStoreDao; + @Inject + private DiskOfferingDao diskOfferingDao; + @Inject + private ApiDispatcher apiDispatcher; + @Inject + private AsyncJobManager asyncJobManager; + + private AsyncJobDispatcher asyncJobDispatcher; + private Timer backupTimer; + private Date currentTimestamp; + + private static Map backupProvidersMap = new HashMap<>(); + private List backupProviders; + + public AsyncJobDispatcher getAsyncJobDispatcher() { + return asyncJobDispatcher; + } + + public void setAsyncJobDispatcher(final AsyncJobDispatcher dispatcher) { + asyncJobDispatcher = dispatcher; + } + + @Override + public List listBackupProviderOfferings(final Long zoneId) { + if (zoneId == null || zoneId < 1) { + throw new CloudRuntimeException("Invalid zone ID passed"); + } + validateForZone(zoneId); + final Account account = CallContext.current().getCallingAccount(); + if (!accountService.isRootAdmin(account.getId())) { + throw new PermissionDeniedException("Parameter external can only be specified by a Root Admin, permission denied"); + } + final BackupProvider backupProvider = getBackupProvider(zoneId); + LOG.debug("Listing external backup offerings for the backup provider configured for zone ID " + zoneId); + return backupProvider.listBackupOfferings(zoneId); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_IMPORT_OFFERING, eventDescription = "importing backup offering", async = true) + public BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd) { + validateForZone(cmd.getZoneId()); + final BackupOffering existingOffering = backupOfferingDao.findByExternalId(cmd.getExternalId(), cmd.getZoneId()); + if (existingOffering != null) { + throw new CloudRuntimeException("A backup offering with external ID " + cmd.getExternalId() + " already exists"); + } + if (backupOfferingDao.findByName(cmd.getName(), cmd.getZoneId()) != null) { + throw new CloudRuntimeException("A backup offering with the same name already exists in this zone"); + } + + final BackupProvider provider = getBackupProvider(cmd.getZoneId()); + if (!provider.isValidProviderOffering(cmd.getZoneId(), cmd.getExternalId())) { + throw new CloudRuntimeException("Backup offering '" + cmd.getExternalId() + "' does not exist on provider " + provider.getName() + " on zone " + cmd.getZoneId()); + } + + final BackupOfferingVO offering = new BackupOfferingVO(cmd.getZoneId(), cmd.getExternalId(), provider.getName(), + cmd.getName(), cmd.getDescription(), cmd.getUserDrivenBackups()); + + final BackupOfferingVO savedOffering = backupOfferingDao.persist(offering); + if (savedOffering == null) { + throw new CloudRuntimeException("Unable to create backup offering: " + cmd.getExternalId() + ", name: " + cmd.getName()); + } + LOG.debug("Successfully created backup offering " + cmd.getName() + " mapped to backup provider offering " + cmd.getExternalId()); + return savedOffering; + } + + @Override + public Pair, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) { + final Long offeringId = cmd.getOfferingId(); + final Long zoneId = cmd.getZoneId(); + final String keyword = cmd.getKeyword(); + + if (offeringId != null) { + BackupOfferingVO offering = backupOfferingDao.findById(offeringId); + if (offering == null) { + throw new CloudRuntimeException("Offering ID " + offeringId + " does not exist"); + } + return new Pair<>(Collections.singletonList(offering), 1); + } + + final Filter searchFilter = new Filter(BackupOfferingVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + SearchBuilder sb = backupOfferingDao.createSearchBuilder(); + sb.and("zone_id", sb.entity().getZoneId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE); + + final SearchCriteria sc = sb.create(); + + if (zoneId != null) { + sc.setParameters("zone_id", zoneId); + } + + if (keyword != null) { + sc.setParameters("name", "%" + keyword + "%"); + } + Pair, Integer> result = backupOfferingDao.searchAndCount(sc, searchFilter); + return new Pair<>(new ArrayList<>(result.first()), result.second()); + } + + @Override + public boolean deleteBackupOffering(final Long offeringId) { + final BackupOfferingVO offering = backupOfferingDao.findById(offeringId); + if (offering == null) { + throw new CloudRuntimeException("Could not find a backup offering with id: " + offeringId); + } + + if (vmInstanceDao.listByZoneWithBackups(offering.getZoneId(), offering.getId()).size() > 0) { + throw new CloudRuntimeException("Backup offering is assigned to VMs, remove the assignment(s) in order to remove the offering."); + } + + validateForZone(offering.getZoneId()); + return backupOfferingDao.remove(offering.getId()); + } + + private String createVolumeInfoFromVolumes(List vmVolumes) { + List list = new ArrayList<>(); + for (VolumeVO vol : vmVolumes) { + list.add(new Backup.VolumeInfo(vol.getUuid(), vol.getPath(), vol.getVolumeType(), vol.getSize())); + } + return new Gson().toJson(list.toArray(), Backup.VolumeInfo[].class); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_OFFERING_ASSIGN, eventDescription = "assign VM to backup offering", async = true) + public boolean assignVMToBackupOffering(Long vmId, Long offeringId) { + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null) { + throw new CloudRuntimeException("Did not find VM by provided ID"); + } + + if (!Arrays.asList(VirtualMachine.State.Running, VirtualMachine.State.Stopped, VirtualMachine.State.Shutdown).contains(vm.getState())) { + throw new CloudRuntimeException("VM is not in running or stopped state"); + } + + validateForZone(vm.getDataCenterId()); + + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + if (vm.getBackupOfferingId() != null) { + throw new CloudRuntimeException("VM already is assigned to a backup offering, please remove the VM from its previous offering"); + } + + final BackupOfferingVO offering = backupOfferingDao.findById(offeringId); + if (offering == null) { + throw new CloudRuntimeException("Provided backup offering does not exist"); + } + + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + if (backupProvider == null) { + throw new CloudRuntimeException("Failed to get the backup provider for the zone, please contact the administrator"); + } + + vm.setBackupOfferingId(offering.getId()); + vm.setBackupVolumes(createVolumeInfoFromVolumes(volumeDao.findByInstance(vm.getId()))); + if (vmInstanceDao.update(vm.getId(), vm)) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_OFFERING_ASSIGN, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), + "Backup-" + vm.getHostName() + "-" + vm.getUuid(), vm.getBackupOfferingId(), null, null, + Backup.class.getSimpleName(), vm.getUuid()); + } else { + throw new CloudRuntimeException("Failed to update VM assignment to the backup offering in the DB, please try again."); + } + + try { + if (backupProvider.assignVMToBackupOffering(vm, offering)) { + return vmInstanceDao.update(vm.getId(), vm); + } + } catch (Exception e) { + LOG.error("Exception caught while assigning VM to backup offering by the backup provider", e); + } + throw new CloudRuntimeException("Failed to assign the VM to the backup offering, please try removing the assignment and try again."); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_OFFERING_REMOVE, eventDescription = "remove VM from backup offering", async = true) + public boolean removeVMFromBackupOffering(final Long vmId, final boolean forced) { + final VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(vmId); + if (vm == null) { + throw new CloudRuntimeException("Did not find VM by provided ID"); + } + + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + final BackupOfferingVO offering = backupOfferingDao.findById(vm.getBackupOfferingId()); + if (offering == null) { + throw new CloudRuntimeException("No previously configured backup offering found for the VM"); + } + + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + if (backupProvider == null) { + throw new CloudRuntimeException("Failed to get the backup provider for the zone, please contact the administrator"); + } + + if (!forced && backupProvider.willDeleteBackupsOnOfferingRemoval()) { + throw new CloudRuntimeException("The backend provider will only allow removal of VM from the offering if forced:true is provided " + + "that will also delete the backups."); + } + + boolean result = false; + VMInstanceVO vmInstance = null; + try { + vmInstance = vmInstanceDao.acquireInLockTable(vm.getId()); + vmInstance.setBackupOfferingId(null); + vmInstance.setBackupExternalId(null); + vmInstance.setBackupVolumes(null); + result = backupProvider.removeVMFromBackupOffering(vmInstance); + if (result && backupProvider.willDeleteBackupsOnOfferingRemoval()) { + final List backups = backupDao.listByVmId(null, vm.getId()); + for (final Backup backup : backups) { + backupDao.remove(backup.getId()); + } + } + if ((result || forced) && vmInstanceDao.update(vmInstance.getId(), vmInstance)) { + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_OFFERING_REMOVE, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), + "Backup-" + vm.getHostName() + "-" + vm.getUuid(), vm.getBackupOfferingId(), null, null, + Backup.class.getSimpleName(), vm.getUuid()); + final BackupSchedule backupSchedule = backupScheduleDao.findByVM(vmInstance.getId()); + if (backupSchedule != null) { + backupScheduleDao.remove(backupSchedule.getId()); + } + result = true; + } + } catch (final Exception e) { + LOG.warn("Exception caught when trying to remove VM from the backup offering: ", e); + } finally { + if (vmInstance != null) { + vmInstanceDao.releaseFromLockTable(vmInstance.getId()); + } + } + return result; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_SCHEDULE_CONFIGURE, eventDescription = "configuring VM backup schedule") + public BackupSchedule configureBackupSchedule(CreateBackupScheduleCmd cmd) { + final Long vmId = cmd.getVmId(); + final DateUtil.IntervalType intervalType = cmd.getIntervalType(); + final String scheduleString = cmd.getSchedule(); + final TimeZone timeZone = TimeZone.getTimeZone(cmd.getTimezone()); + + if (intervalType == null) { + throw new CloudRuntimeException("Invalid interval type provided"); + } + + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null) { + throw new CloudRuntimeException("Did not find VM by provided ID"); + } + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + if (vm.getBackupOfferingId() == null) { + throw new CloudRuntimeException("Cannot configure backup schedule for the VM without having any backup offering"); + } + + final BackupOffering offering = backupOfferingDao.findById(vm.getBackupOfferingId()); + if (offering == null || !offering.isUserDrivenBackupAllowed()) { + throw new CloudRuntimeException("The selected backup offering does not allow user-defined backup schedule"); + } + + final String timezoneId = timeZone.getID(); + if (!timezoneId.equals(cmd.getTimezone())) { + LOG.warn("Using timezone: " + timezoneId + " for running this snapshot policy as an equivalent of " + cmd.getTimezone()); + } + + Date nextDateTime = null; + try { + nextDateTime = DateUtil.getNextRunTime(intervalType, cmd.getSchedule(), timezoneId, null); + } catch (Exception e) { + throw new InvalidParameterValueException("Invalid schedule: " + cmd.getSchedule() + " for interval type: " + cmd.getIntervalType()); + } + + final BackupScheduleVO schedule = backupScheduleDao.findByVM(vmId); + if (schedule == null) { + return backupScheduleDao.persist(new BackupScheduleVO(vmId, intervalType, scheduleString, timezoneId, nextDateTime)); + } + + schedule.setScheduleType((short) intervalType.ordinal()); + schedule.setSchedule(scheduleString); + schedule.setTimezone(timezoneId); + schedule.setScheduledTimestamp(nextDateTime); + backupScheduleDao.update(schedule.getId(), schedule); + return backupScheduleDao.findByVM(vmId); + } + + @Override + public BackupSchedule listBackupSchedule(final Long vmId) { + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null) { + throw new CloudRuntimeException("Did not find VM by provided ID"); + } + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + return backupScheduleDao.findByVM(vmId); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_SCHEDULE_DELETE, eventDescription = "deleting VM backup schedule") + public boolean deleteBackupSchedule(final Long vmId) { + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null) { + throw new CloudRuntimeException("Did not find VM by provided ID"); + } + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + final BackupSchedule schedule = backupScheduleDao.findByVM(vmId); + if (schedule == null) { + throw new CloudRuntimeException("VM has no backup schedule defined, no need to delete anything."); + } + return backupScheduleDao.remove(schedule.getId()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_CREATE, eventDescription = "creating VM backup", async = true) + public boolean createBackup(final Long vmId) { + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null) { + throw new CloudRuntimeException("Did not find VM by provided ID"); + } + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + if (vm.getBackupOfferingId() == null) { + throw new CloudRuntimeException("VM has not backup offering configured, cannot create backup before assigning it to a backup offering"); + } + + final BackupOffering offering = backupOfferingDao.findById(vm.getBackupOfferingId()); + if (offering == null) { + throw new CloudRuntimeException("VM backup offering not found"); + } + + if (!offering.isUserDrivenBackupAllowed()) { + throw new CloudRuntimeException("The assigned backup offering does not allow ad-hoc user backup"); + } + + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + if (backupProvider != null && backupProvider.takeBackup(vm)) { + return true; + } + throw new CloudRuntimeException("Failed to create VM backup"); + } + + @Override + public Pair, Integer> listBackups(final ListBackupsCmd cmd) { + final Long id = cmd.getId(); + final Long vmId = cmd.getVmId(); + final Long zoneId = cmd.getZoneId(); + final Account caller = CallContext.current().getCallingAccount(); + final String keyword = cmd.getKeyword(); + List permittedAccounts = new ArrayList(); + + if (vmId != null) { + VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(vmId); + if (vm != null) { + accountManager.checkAccess(caller, null, true, vm); + } + } + + final Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), + cmd.isRecursive(), null); + accountManager.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + final Long domainId = domainIdRecursiveListProject.first(); + final Boolean isRecursive = domainIdRecursiveListProject.second(); + final Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + + final Filter searchFilter = new Filter(BackupVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + SearchBuilder sb = backupDao.createSearchBuilder(); + accountManager.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("idIN", sb.entity().getId(), SearchCriteria.Op.IN); + sb.and("vmId", sb.entity().getVmId(), SearchCriteria.Op.EQ); + sb.and("zoneId", sb.entity().getZoneId(), SearchCriteria.Op.EQ); + + if (keyword != null) { + SearchBuilder vmSearch = vmInstanceDao.createSearchBuilder(); + vmSearch.and("name", vmSearch.entity().getHostName(), SearchCriteria.Op.LIKE); + sb.groupBy(sb.entity().getId()); + sb.join("vmSearch", vmSearch, sb.entity().getVmId(), vmSearch.entity().getId(), JoinBuilder.JoinType.INNER); + } + + SearchCriteria sc = sb.create(); + accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + + if (id != null) { + sc.setParameters("id", id); + } + + if (vmId != null) { + sc.setParameters("vmId", vmId); + } + + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + + if (keyword != null) { + sc.setJoinParameters("vmSearch", "name", "%" + keyword + "%"); + } + + Pair, Integer> result = backupDao.searchAndCount(sc, searchFilter); + return new Pair<>(new ArrayList<>(result.first()), result.second()); + } + + public boolean importRestoredVM(long zoneId, long domainId, long accountId, long userId, + String vmInternalName, Hypervisor.HypervisorType hypervisorType, Backup backup) { + VirtualMachine vm = null; + HypervisorGuru guru = hypervisorGuruManager.getGuru(hypervisorType); + try { + vm = guru.importVirtualMachineFromBackup(zoneId, domainId, accountId, userId, vmInternalName, backup); + } catch (final Exception e) { + LOG.error("Failed to import VM from backup restoration", e); + throw new CloudRuntimeException("Error during vm backup restoration and import: " + e.getMessage()); + } + if (vm == null) { + LOG.error("Failed to import restored VM " + vmInternalName + " with hypervisor type " + hypervisorType + " using backup of VM ID " + backup.getVmId()); + } + return vm != null; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_RESTORE, eventDescription = "restoring VM from backup", async = true) + public boolean restoreBackup(final Long backupId) { + final BackupVO backup = backupDao.findById(backupId); + if (backup == null) { + throw new CloudRuntimeException("Backup " + backupId + " does not exist"); + } + validateForZone(backup.getZoneId()); + + final VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); + if (vm == null) { + throw new CloudRuntimeException("VM ID " + backup.getVmId() + " couldn't be found on existing or removed VMs"); + } + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + if (vm.getRemoved() == null && !vm.getState().equals(VirtualMachine.State.Stopped) && + !vm.getState().equals(VirtualMachine.State.Destroyed)) { + throw new CloudRuntimeException("Existing VM should be stopped before being restored from backup"); + } + + final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId()); + if (offering == null) { + throw new CloudRuntimeException("Failed to find backup offering of the VM backup"); + } + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + if (!backupProvider.restoreVMFromBackup(vm, backup)) { + throw new CloudRuntimeException("Error restoring VM from backup ID " + backup.getId()); + } + return importRestoredVM(vm.getDataCenterId(), vm.getDomainId(), vm.getAccountId(), vm.getUserId(), + vm.getInstanceName(), vm.getHypervisorType(), backup); + } + + private Backup.VolumeInfo getVolumeInfo(List backedUpVolumes, String volumeUuid) { + for (Backup.VolumeInfo volInfo : backedUpVolumes) { + if (volInfo.getUuid().equals(volumeUuid)) { + return volInfo; + } + } + return null; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_RESTORE, eventDescription = "restoring VM from backup", async = true) + public boolean restoreBackupVolumeAndAttachToVM(final String backedUpVolumeUuid, final Long backupId, final Long vmId) throws Exception { + if (Strings.isNullOrEmpty(backedUpVolumeUuid)) { + throw new CloudRuntimeException("Invalid volume ID passed"); + } + final BackupVO backup = backupDao.findById(backupId); + if (backup == null) { + throw new CloudRuntimeException("Provided backup not found"); + } + validateForZone(backup.getZoneId()); + + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null) { + throw new CloudRuntimeException("Provided VM not found"); + } + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + if (vm.getBackupOfferingId() != null) { + throw new CloudRuntimeException("The selected VM has backups, cannot restore and attach volume to the VM."); + } + + if (backup.getZoneId() != vm.getDataCenterId()) { + throw new CloudRuntimeException("Cross zone backup restoration of volume is not allowed"); + } + + final VMInstanceVO vmFromBackup = vmInstanceDao.findByIdIncludingRemoved(backup.getVmId()); + if (vmFromBackup == null) { + throw new CloudRuntimeException("VM reference for the provided VM backup not found"); + } + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vmFromBackup); + + Pair restoreInfo = getRestoreVolumeHostAndDatastore(vm); + String hostIp = restoreInfo.first(); + String datastoreUuid = restoreInfo.second(); + + LOG.debug("Asking provider to restore volume " + backedUpVolumeUuid + " from backup " + backupId + + " (with external ID " + backup.getExternalId() + ") and attach it to VM: " + vm.getUuid()); + + final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(backup.getBackupOfferingId()); + if (offering == null) { + throw new CloudRuntimeException("Failed to find VM backup offering"); + } + + BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + Pair result = backupProvider.restoreBackedUpVolume(backup, backedUpVolumeUuid, hostIp, datastoreUuid); + if (!result.first()) { + throw new CloudRuntimeException("Error restoring volume " + backedUpVolumeUuid); + } + if (!attachVolumeToVM(vm.getDataCenterId(), result.second(), vmFromBackup.getBackupVolumeList(), + backedUpVolumeUuid, vm, datastoreUuid, backup)) { + throw new CloudRuntimeException("Error attaching volume " + backedUpVolumeUuid + " to VM " + vm.getUuid()); + } + return true; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_DELETE, eventDescription = "deleting VM backup", async = true) + public boolean deleteBackup(final Long backupId) { + final BackupVO backup = backupDao.findByIdIncludingRemoved(backupId); + if (backup == null) { + throw new CloudRuntimeException("Backup " + backupId + " does not exist"); + } + final Long vmId = backup.getVmId(); + final VMInstanceVO vm = vmInstanceDao.findByIdIncludingRemoved(vmId); + if (vm == null) { + throw new CloudRuntimeException("VM " + vmId + " does not exist"); + } + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + final BackupOffering offering = backupOfferingDao.findByIdIncludingRemoved(vm.getBackupOfferingId()); + if (offering == null) { + throw new CloudRuntimeException("VM backup offering ID " + vm.getBackupOfferingId() + " does not exist"); + } + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); + boolean result = backupProvider.deleteBackup(backup); + if (result) { + return backupDao.remove(backup.getId()); + } + throw new CloudRuntimeException("Failed to delete the backup"); + } + + /** + * Get the pair: hostIp, datastoreUuid in which to restore the volume, based on the VM to be attached information + */ + private Pair getRestoreVolumeHostAndDatastore(VMInstanceVO vm) { + List rootVmVolume = volumeDao.findIncludingRemovedByInstanceAndType(vm.getId(), Volume.Type.ROOT); + Long poolId = rootVmVolume.get(0).getPoolId(); + StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(poolId); + String datastoreUuid = storagePoolVO.getUuid(); + String hostIp = vm.getHostId() == null ? + getHostIp(storagePoolVO) : + hostDao.findById(vm.getHostId()).getPrivateIpAddress(); + return new Pair<>(hostIp, datastoreUuid); + } + + /** + * Find a host IP from storage pool access + */ + private String getHostIp(StoragePoolVO storagePoolVO) { + List hosts = null; + if (storagePoolVO.getScope().equals(ScopeType.CLUSTER)) { + hosts = hostDao.findByClusterId(storagePoolVO.getClusterId()); + + } else if (storagePoolVO.getScope().equals(ScopeType.ZONE)) { + hosts = hostDao.findByDataCenterId(storagePoolVO.getDataCenterId()); + } + return hosts.get(0).getPrivateIpAddress(); + } + + /** + * Attach volume to VM + */ + private boolean attachVolumeToVM(Long zoneId, String restoredVolumeLocation, List backedUpVolumes, + String volumeUuid, VMInstanceVO vm, String datastoreUuid, Backup backup) throws Exception { + HypervisorGuru guru = hypervisorGuruManager.getGuru(vm.getHypervisorType()); + Backup.VolumeInfo volumeInfo = getVolumeInfo(backedUpVolumes, volumeUuid); + if (volumeInfo == null) { + throw new CloudRuntimeException("Failed to find volume in the backedup volumes of ID " + volumeUuid); + } + volumeInfo.setType(Volume.Type.DATADISK); + + LOG.debug("Attaching the restored volume to VM " + vm.getId()); + StoragePoolVO pool = primaryDataStoreDao.findByUuid(datastoreUuid); + try { + return guru.attachRestoredVolumeToVirtualMachine(zoneId, restoredVolumeLocation, volumeInfo, vm, pool.getId(), backup); + } catch (Exception e) { + throw new CloudRuntimeException("Error attach restored volume to VM " + vm.getUuid() + " due to: " + e.getMessage()); + } + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + backgroundPollManager.submitTask(new BackupSyncTask(this)); + return true; + } + + public boolean isDisabled(final Long zoneId) { + return !BackupFrameworkEnabled.valueIn(zoneId); + } + + private void validateForZone(final Long zoneId) { + if (zoneId == null || isDisabled(zoneId)) { + throw new CloudRuntimeException("Backup and Recovery feature is disabled for the zone"); + } + } + + @Override + public List listBackupProviders() { + return backupProviders; + } + + @Override + public BackupProvider getBackupProvider(final Long zoneId) { + final String name = BackupProviderPlugin.valueIn(zoneId); + return getBackupProvider(name); + } + + public BackupProvider getBackupProvider(final String name) { + if (Strings.isNullOrEmpty(name)) { + throw new CloudRuntimeException("Invalid backup provider name provided"); + } + if (!backupProvidersMap.containsKey(name)) { + throw new CloudRuntimeException("Failed to find backup provider by the name: " + name); + } + return backupProvidersMap.get(name); + } + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList>(); + // Offerings + cmdList.add(ListBackupProvidersCmd.class); + cmdList.add(ListBackupProviderOfferingsCmd.class); + cmdList.add(ImportBackupOfferingCmd.class); + cmdList.add(ListBackupOfferingsCmd.class); + cmdList.add(DeleteBackupOfferingCmd.class); + // Assignment + cmdList.add(AssignVirtualMachineToBackupOfferingCmd.class); + cmdList.add(RemoveVirtualMachineFromBackupOfferingCmd.class); + // Schedule + cmdList.add(CreateBackupScheduleCmd.class); + cmdList.add(UpdateBackupScheduleCmd.class); + cmdList.add(ListBackupScheduleCmd.class); + cmdList.add(DeleteBackupScheduleCmd.class); + // Operations + cmdList.add(CreateBackupCmd.class); + cmdList.add(ListBackupsCmd.class); + cmdList.add(RestoreBackupCmd.class); + cmdList.add(DeleteBackupCmd.class); + cmdList.add(RestoreVolumeFromBackupAndAttachToVMCmd.class); + return cmdList; + } + + @Override + public String getConfigComponentName() { + return BackupService.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + BackupFrameworkEnabled, + BackupProviderPlugin, + BackupSyncPollingInterval + }; + } + + public void setBackupProviders(final List backupProviders) { + this.backupProviders = backupProviders; + } + + private void initializeBackupProviderMap() { + if (backupProviders != null) { + for (final BackupProvider backupProvider : backupProviders) { + backupProvidersMap.put(backupProvider.getName().toLowerCase(), backupProvider); + } + } + } + + public void poll(final Date timestamp) { + currentTimestamp = timestamp; + GlobalLock scanLock = GlobalLock.getInternLock("backup.poll"); + try { + if (scanLock.lock(5)) { + try { + checkStatusOfCurrentlyExecutingBackups(); + } finally { + scanLock.unlock(); + } + } + } finally { + scanLock.releaseRef(); + } + + scanLock = GlobalLock.getInternLock("backup.poll"); + try { + if (scanLock.lock(5)) { + try { + scheduleBackups(); + } finally { + scanLock.unlock(); + } + } + } finally { + scanLock.releaseRef(); + } + } + + @DB + private Date scheduleNextBackupJob(final BackupScheduleVO backupSchedule) { + final Date nextTimestamp = DateUtil.getNextRunTime(backupSchedule.getScheduleType(), backupSchedule.getSchedule(), + backupSchedule.getTimezone(), currentTimestamp); + return Transaction.execute(new TransactionCallback() { + @Override + public Date doInTransaction(TransactionStatus status) { + backupSchedule.setScheduledTimestamp(nextTimestamp); + backupSchedule.setAsyncJobId(null); + backupScheduleDao.update(backupSchedule.getId(), backupSchedule); + return nextTimestamp; + } + }); + } + + private void checkStatusOfCurrentlyExecutingBackups() { + final SearchCriteria sc = backupScheduleDao.createSearchCriteria(); + sc.addAnd("asyncJobId", SearchCriteria.Op.NNULL); + final List backupSchedules = backupScheduleDao.search(sc, null); + for (final BackupScheduleVO backupSchedule : backupSchedules) { + final Long asyncJobId = backupSchedule.getAsyncJobId(); + final AsyncJobVO asyncJob = asyncJobManager.getAsyncJob(asyncJobId); + switch (asyncJob.getStatus()) { + case SUCCEEDED: + case FAILED: + final Date nextDateTime = scheduleNextBackupJob(backupSchedule); + final String nextScheduledTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, nextDateTime); + LOG.debug("Next backup scheduled time for VM ID " + backupSchedule.getVmId() + " is " + nextScheduledTime); + break; + } + } + } + + @DB + public void scheduleBackups() { + String displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, currentTimestamp); + LOG.debug("Backup backup.poll is being called at " + displayTime); + + final List backupsToBeExecuted = backupScheduleDao.getSchedulesToExecute(currentTimestamp); + for (final BackupScheduleVO backupSchedule: backupsToBeExecuted) { + final Long backupScheduleId = backupSchedule.getId(); + final Long vmId = backupSchedule.getVmId(); + + final VMInstanceVO vm = vmInstanceDao.findById(vmId); + if (vm == null || vm.getBackupOfferingId() == null) { + backupScheduleDao.remove(backupScheduleId); + continue; + } + + final BackupOffering offering = backupOfferingDao.findById(vm.getBackupOfferingId()); + if (offering == null || !offering.isUserDrivenBackupAllowed()) { + continue; + } + + if (isDisabled(vm.getDataCenterId())) { + continue; + } + + final Account backupAccount = accountService.getAccount(vm.getAccountId()); + if (backupAccount == null || backupAccount.getState() == Account.State.disabled) { + if (LOG.isDebugEnabled()) { + LOG.debug("Skip backup for VM " + vm.getUuid() + " since its account has been removed or disabled"); + } + continue; + } + + if (LOG.isDebugEnabled()) { + final Date scheduledTimestamp = backupSchedule.getScheduledTimestamp(); + displayTime = DateUtil.displayDateInTimezone(DateUtil.GMT_TIMEZONE, scheduledTimestamp); + LOG.debug("Scheduling 1 backup for VM ID " + vm.getId() + " (VM name:" + vm.getHostName() + + ") for backup schedule id: " + backupSchedule.getId() + " at " + displayTime); + } + + BackupScheduleVO tmpBackupScheduleVO = null; + + try { + tmpBackupScheduleVO = backupScheduleDao.acquireInLockTable(backupScheduleId); + + final Long eventId = ActionEventUtils.onScheduledActionEvent(User.UID_SYSTEM, vm.getAccountId(), + EventTypes.EVENT_VM_BACKUP_CREATE, "creating backup for VM ID:" + vm.getUuid(), true, 0); + final Map params = new HashMap(); + params.put(ApiConstants.VIRTUAL_MACHINE_ID, "" + vmId); + params.put("ctxUserId", "1"); + params.put("ctxAccountId", "" + vm.getAccountId()); + params.put("ctxStartEventId", String.valueOf(eventId)); + + final CreateBackupCmd cmd = new CreateBackupCmd(); + ComponentContext.inject(cmd); + apiDispatcher.dispatchCreateCmd(cmd, params); + params.put("id", "" + vmId); + params.put("ctxStartEventId", "1"); + + AsyncJobVO job = new AsyncJobVO("", User.UID_SYSTEM, vm.getAccountId(), CreateBackupCmd.class.getName(), + ApiGsonHelper.getBuilder().create().toJson(params), vmId, + cmd.getInstanceType() != null ? cmd.getInstanceType().toString() : null, null); + job.setDispatcher(asyncJobDispatcher.getName()); + + final long jobId = asyncJobManager.submitAsyncJob(job); + tmpBackupScheduleVO.setAsyncJobId(jobId); + backupScheduleDao.update(backupScheduleId, tmpBackupScheduleVO); + } catch (Exception e) { + LOG.warn("Scheduling backup failed due to ", e); + } finally { + if (tmpBackupScheduleVO != null) { + backupScheduleDao.releaseFromLockTable(backupScheduleId); + } + } + } + } + + @Override + public boolean start() { + initializeBackupProviderMap(); + + currentTimestamp = new Date(); + for (final BackupScheduleVO backupSchedule : backupScheduleDao.listAll()) { + scheduleNextBackupJob(backupSchedule); + } + final TimerTask backupPollTask = new ManagedContextTimerTask() { + @Override + protected void runInContext() { + try { + poll(new Date()); + } catch (final Throwable t) { + LOG.warn("Catch throwable in backup scheduler ", t); + } + } + }; + + backupTimer = new Timer("BackupPollTask"); + backupTimer.schedule(backupPollTask, BackupSyncPollingInterval.value() * 1000L, BackupSyncPollingInterval.value() * 1000L); + return true; + } + + //////////////////////////////////////////////////// + /////////////// Background Tasks /////////////////// + //////////////////////////////////////////////////// + + /** + * This background task syncs backups from providers side in CloudStack db + * along with creation of usage records + */ + private final class BackupSyncTask extends ManagedContextRunnable implements BackgroundPollTask { + private BackupManager backupManager; + + public BackupSyncTask(final BackupManager backupManager) { + this.backupManager = backupManager; + } + + @Override + protected void runInContext() { + final int SYNC_INTERVAL = BackupSyncPollingInterval.value().intValue(); + try { + if (LOG.isTraceEnabled()) { + LOG.trace("Backup sync background task is running..."); + } + for (final DataCenter dataCenter : dataCenterDao.listAllZones()) { + if (dataCenter == null || isDisabled(dataCenter.getId())) { + continue; + } + + final BackupProvider backupProvider = getBackupProvider(dataCenter.getId()); + if (backupProvider == null) { + LOG.warn("Backup provider not available or configured for zone ID " + dataCenter.getId()); + continue; + } + + List vms = vmInstanceDao.listByZoneWithBackups(dataCenter.getId(), null); + if (vms == null || vms.isEmpty()) { + continue; + } + + // Sync backup usage metrics + final Map metrics = backupProvider.getBackupMetrics(dataCenter.getId(), new ArrayList<>(vms)); + final GlobalLock syncBackupMetricsLock = GlobalLock.getInternLock("BackupSyncTask_metrics_zone_" + dataCenter.getId()); + if (syncBackupMetricsLock.lock(SYNC_INTERVAL)) { + try { + for (final VirtualMachine vm : metrics.keySet()) { + final Backup.Metric metric = metrics.get(vm); + if (metric != null) { + usageBackupDao.updateMetrics(vm, metric); + } + } + } finally { + syncBackupMetricsLock.unlock(); + } + } + + // Sync out-of-band backups + for (final VirtualMachine vm : vms) { + final GlobalLock syncBackupsLock = GlobalLock.getInternLock("BackupSyncTask_backup_vm_" + vm.getId()); + if (syncBackupsLock.lock(SYNC_INTERVAL)) { + try { + backupProvider.syncBackups(vm, metrics.get(vm)); + } finally { + syncBackupsLock.unlock(); + } + } + } + } + } catch (final Throwable t) { + LOG.error("Error trying to run backup-sync background task", t); + } + } + + @Override + public Long getDelay() { + return BackupSyncPollingInterval.value() * 1000L; + } + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index f3525cce6b1..17a9b946dd8 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -301,6 +301,11 @@ + + + + + diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 152cbb09151..8ec24afe938 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.when; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -508,6 +509,8 @@ public class VolumeApiServiceImplTest { when(vm.getType()).thenReturn(VirtualMachine.Type.User); when(vm.getState()).thenReturn(State.Running); when(vm.getDataCenterId()).thenReturn(34L); + when(vm.getBackupOfferingId()).thenReturn(null); + when(vm.getBackupVolumeList()).thenReturn(Collections.emptyList()); when(volumeDaoMock.findByInstanceAndType(anyLong(), any(Volume.Type.class))).thenReturn(new ArrayList<>(10)); when(volumeDataFactoryMock.getVolume(9L)).thenReturn(volumeToAttach); when(volumeToAttach.getState()).thenReturn(Volume.State.Uploaded); diff --git a/server/src/test/java/com/cloud/user/MockUsageEventDao.java b/server/src/test/java/com/cloud/user/MockUsageEventDao.java index ab844ee69e3..5d8c62115ec 100644 --- a/server/src/test/java/com/cloud/user/MockUsageEventDao.java +++ b/server/src/test/java/com/cloud/user/MockUsageEventDao.java @@ -216,6 +216,11 @@ public class MockUsageEventDao implements UsageEventDao{ } + @Override + public boolean unremove(Long id) { + return false; + } + @Override public K getNextInSequence(Class clazz, String name) { return null; diff --git a/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java b/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java index 12e713585f0..1a7c1f70090 100644 --- a/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java +++ b/server/src/test/java/com/cloud/vpc/dao/MockNetworkDaoImpl.java @@ -236,6 +236,11 @@ public class MockNetworkDaoImpl extends GenericDaoBase implemen return null; } + @Override + public NetworkVO findByVlan(String vlan) { + return null; + } + @Override public List listByAccountIdNetworkName(final long accountId, final String name) { return null; diff --git a/test/integration/smoke/test_backup_recovery_dummy.py b/test/integration/smoke/test_backup_recovery_dummy.py new file mode 100644 index 00000000000..79f375c605b --- /dev/null +++ b/test/integration/smoke/test_backup_recovery_dummy.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# 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. + +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import (cleanup_resources) +from marvin.lib.base import (Account, ServiceOffering, VirtualMachine, BackupOffering, Configurations, Backup) +from marvin.lib.common import (get_domain, get_zone, get_template) +from nose.plugins.attrib import attr +from marvin.codes import FAILED + +class TestDummyBackupAndRecovery(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + # Setup + + cls.testClient = super(TestDummyBackupAndRecovery, cls).getClsTestClient() + cls.api_client = cls.testClient.getApiClient() + cls.services = cls.testClient.getParsedTestDataConfig() + cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + cls.services["mode"] = cls.zone.networktype + cls.hypervisor = cls.testClient.getHypervisorInfo() + cls.domain = get_domain(cls.api_client) + cls.template = get_template(cls.api_client, cls.zone.id, cls.services["ostype"]) + if cls.template == FAILED: + assert False, "get_template() failed to return template with description %s" % cls.services["ostype"] + cls.services["small"]["zoneid"] = cls.zone.id + cls.services["small"]["template"] = cls.template.id + cls.account = Account.create(cls.api_client, cls.services["account"], domainid=cls.domain.id) + cls.offering = ServiceOffering.create(cls.api_client,cls.services["service_offerings"]["small"]) + cls.vm = VirtualMachine.create(cls.api_client, cls.services["small"], accountid=cls.account.name, + domainid=cls.account.domainid, serviceofferingid=cls.offering.id, + mode=cls.services["mode"]) + cls._cleanup = [cls.offering, cls.account] + + # Check backup configuration values, set them to enable the dummy provider + + backup_enabled_cfg = Configurations.list(cls.api_client, name='backup.framework.enabled', zoneid=cls.zone.id) + backup_provider_cfg = Configurations.list(cls.api_client, name='backup.framework.provider.plugin', zoneid=cls.zone.id) + cls.backup_enabled = backup_enabled_cfg[0].value + cls.backup_provider = backup_provider_cfg[0].value + + if cls.backup_enabled == "false": + Configurations.update(cls.api_client, 'backup.framework.enabled', value='true', zoneid=cls.zone.id) + if cls.backup_provider != "dummy": + Configurations.update(cls.api_client, 'backup.framework.provider.plugin', value='dummy', zoneid=cls.zone.id) + + # Import a dummy backup offering to use on tests + + cls.provider_offerings = BackupOffering.listExternal(cls.api_client, cls.zone.id) + cls.debug("Importing backup offering %s - %s" % (cls.provider_offerings[0].externalid, cls.provider_offerings[0].name)) + cls.offering = BackupOffering.importExisting(cls.api_client, cls.zone.id, cls.provider_offerings[0].externalid, + cls.provider_offerings[0].name, cls.provider_offerings[0].description) + cls._cleanup.append(cls.offering) + + @classmethod + def tearDownClass(cls): + try: + # Cleanup resources used + cleanup_resources(cls.api_client, cls._cleanup) + + # Restore original backup framework values values + if cls.backup_enabled == "false": + Configurations.update(cls.api_client, 'backup.framework.enabled', value=cls.backup_enabled, zoneid=cls.zone.id) + if cls.backup_provider != "dummy": + Configurations.update(cls.api_client, 'backup.framework.provider.plugin', value=cls.backup_provider, zoneid=cls.zone.id) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + @attr(tags=["advanced", "backup"], required_hardware="false") + def test_import_backup_offering(self): + """ + Import provider backup offering from Dummy Backup and Recovery Provider + """ + + # Import backup offering + provider_offering = self.provider_offerings[1] + self.debug("Importing backup offering %s - %s" % (provider_offering.externalid, provider_offering.name)) + offering = BackupOffering.importExisting(self.apiclient, self.zone.id, provider_offering.externalid, + provider_offering.name, provider_offering.description) + + # Verify offering is listed + imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") + self.assertNotEqual(len(imported_offering), 0, "Check if the list API returns a non-empty response") + matching_offerings = [x for x in imported_offering if x.id == offering.id] + self.assertNotEqual(len(matching_offerings), 0, "Check if there is a matching offering") + + # Delete backup offering + self.debug("Deleting backup offering %s" % offering.id) + offering.delete(self.apiclient) + + # Verify offering is not listed + imported_offering = BackupOffering.listByZone(self.apiclient, self.zone.id) + self.assertIsInstance(imported_offering, list, "List Backup Offerings should return a valid response") + matching_offerings = [x for x in imported_offering if x.id == offering.id] + self.assertEqual(len(matching_offerings), 0, "Check there is not a matching offering") + + @attr(tags=["advanced", "backup"], required_hardware="false") + def test_vm_backup_lifecycle(self): + """ + Test VM backup lifecycle + """ + + # Verify there are no backups for the VM + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Assign VM to offering and create ad-hoc backup + self.offering.assignOffering(self.apiclient, self.vm.id) + Backup.create(self.apiclient, self.vm.id) + + # Verify backup is created for the VM + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(len(backups), 1, "There should exist only one backup for the VM") + backup = backups[0] + + # Delete backup + Backup.delete(self.apiclient, backup.id) + + # Verify backup is deleted + backups = Backup.list(self.apiclient, self.vm.id) + self.assertEqual(backups, None, "There should not exist any backup for the VM") + + # Remove VM from offering + self.offering.removeOffering(self.apiclient, self.vm.id) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 2603552d446..39a41239c2a 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -189,6 +189,8 @@ known_categories = { 'Sioc' : 'Sioc', 'Diagnostics': 'Diagnostics', 'Management': 'Management', + 'Backup' : 'Backup and Recovery', + 'Restore' : 'Backup and Recovery', 'UnmanagedInstance': 'Virtual Machine' } diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 2664419a878..ad2496c4eae 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -5278,3 +5278,106 @@ class ResourceDetails: cmd.resourceid = resourceid cmd.resourcetype = resourcetype return (apiclient.removeResourceDetail(cmd)) + +# Backup and Recovery + +class BackupOffering: + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def importExisting(self, apiclient, zoneid, externalid, name, description, allowuserdrivenbackups=True): + """Import existing backup offering from the provider""" + + cmd = importBackupOffering.importBackupOfferingCmd() + cmd.zoneid = zoneid + cmd.externalid = externalid + cmd.name = name + cmd.description = description + cmd.allowuserdrivenbackups = allowuserdrivenbackups + return BackupOffering(apiclient.importBackupOffering(cmd).__dict__) + + @classmethod + def listById(self, apiclient, id): + """List imported backup policies by id""" + + cmd = listBackupOfferings.listBackupOfferingsCmd() + cmd.id = id + return (apiclient.listBackupOfferings(cmd)) + + @classmethod + def listByZone(self, apiclient, zoneid): + """List imported backup policies""" + + cmd = listBackupOfferings.listBackupOfferingsCmd() + cmd.zoneid = zoneid + return (apiclient.listBackupOfferings(cmd)) + + @classmethod + def listExternal(self, apiclient, zoneid): + """List external backup policies""" + + cmd = listBackupProviderOfferings.listBackupProviderOfferingsCmd() + cmd.zoneid = zoneid + return (apiclient.listBackupProviderOfferings(cmd)) + + def delete(self, apiclient): + """Delete an imported backup offering""" + + cmd = deleteBackupOffering.deleteBackupOfferingCmd() + cmd.id = self.id + return (apiclient.deleteBackupOffering(cmd)) + + def assignOffering(self, apiclient, vmid): + """Add a VM to a backup offering""" + + cmd = assignVirtualMachineToBackupOffering.assignVirtualMachineToBackupOfferingCmd() + cmd.backupofferingid = self.id + cmd.virtualmachineid = vmid + return (apiclient.assignVirtualMachineToBackupOffering(cmd)) + + def removeOffering(self, apiclient, vmid, forced=True): + """Remove a VM from a backup offering""" + + cmd = removeVirtualMachineFromBackupOffering.removeVirtualMachineFromBackupOfferingCmd() + cmd.virtualmachineid = vmid + cmd.forced = forced + return (apiclient.removeVirtualMachineFromBackupOffering(cmd)) + +class Backup: + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def create(self, apiclient, vmid): + """Create VM backup""" + + cmd = createBackup.createBackupCmd() + cmd.virtualmachineid = vmid + return (apiclient.createBackup(cmd)) + + @classmethod + def delete(self, apiclient, id): + """Delete VM backup""" + + cmd = deleteBackup.deleteBackupCmd() + cmd.id = id + return (apiclient.deleteBackup(cmd)) + + @classmethod + def list(self, apiclient, vmid): + """List VM backups""" + + cmd = listBackups.listBackupsCmd() + cmd.virtualmachineid = vmid + cmd.listall = True + return (apiclient.listBackups(cmd)) + + def restoreVM(self, apiclient): + """Restore VM from backup""" + + cmd = restoreBackup.restoreBackupCmd() + cmd.id = self.id + return (apiclient.restoreBackup(cmd)) diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 9de554aee3e..cc69a39cf9e 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -12277,22 +12277,26 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it .stop .icon, .removeVmwareDc .icon, +.removeBackupChain .icon, .release .icon { background-position: 0 -31px; } .stop:hover .icon, .removeVmwareDc:hover .icon, +.removeBackupChain:hover .icon, .release:hover .icon { background-position: 0 -613px; } .restart .icon, +.restoreBackup .icon, .releaseDedicatedZone .icon { background-position: 0 -63px; } .restart:hover .icon, +.restoreBackup:hover .icon, .releaseDedicatedZone:hover .icon { background-position: 0 -645px; } @@ -12354,14 +12358,16 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it .attach .icon, .attachISO .icon, .attachDisk .icon, +.restoreBackupVolume .icon, .associateProfileToBlade .icon { background-position: -104px -3px; } .attach:hover .icon, .attachISO:hover .icon, +.restoreBackupVolume:hover .icon, .attachDisk:hover .icon { - background-position: -101px -585px; + background-position: -104px -585px; } .detach .icon, @@ -12408,21 +12414,25 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it .snapshot .icon, .takeSnapshot .icon, +.startBackup .icon, .storageSnapshot .icon { background-position: -36px -91px; } .snapshot:hover .icon, .takeSnapshot:hover .icon, +.startBackup:hover .icon, .storageSnapshot:hover .icon { background-position: -36px -673px; } -.recurringSnapshot .icon { +.recurringSnapshot .icon, +.configureBackupSchedule .icon { background-position: -69px -95px; } -.recurringSnapshot:hover .icon { +.recurringSnapshot:hover .icon, +.configureBackupSchedule:hover .icon { background-position: -69px -677px; } @@ -12504,6 +12514,7 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it .createTemplate .icon, .enableSwift .icon, .addVM .icon, +.assignToBackupOffering .icon, .dedicateZone .icon, .dedicate .icon { background-position: -69px -63px; @@ -12513,6 +12524,7 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it .createTemplate:hover .icon, .enableSwift:hover .icon, .addVM:hover .icon, +.assignToBackupOffering:hover .icon, .dedicateZone:hover .icon { background-position: -69px -645px; } @@ -12631,11 +12643,13 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it background-position: -138px -647px; } -.cancelMaintenanceMode .icon { +.cancelMaintenanceMode .icon, +.removeFromBackupOffering .icon { background-position: -138px -123px; } -.cancelMaintenanceMode:hover .icon { +.cancelMaintenanceMode:hover .icon, +.removeFromBackupOffering .icon { background-position: -138px -705px; } @@ -12745,10 +12759,12 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it background-position: -197px -647px; } +.createBackup .icon, .forceReconnect .icon { background-position: -196px -95px; } +.createBackup:hover .icon, .forceReconnect:hover .icon { background-position: -196px -677px; } diff --git a/ui/index.html b/ui/index.html index 982a159d2d9..7d24838d40b 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1919,6 +1919,7 @@ + diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 6412a6732fc..fcb6f1eb9aa 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -58,6 +58,7 @@ var dictionary = { "label.ESP.hash":"ESP Hash", "label.ESP.lifetime":"ESP Lifetime (second)", "label.ESP.policy":"ESP policy", +"label.import.backup.offering":"Import Backup Offering", "label.IKE.DH":"IKE DH", "label.IKE.encryption":"IKE Encryption", "label.IKE.hash":"IKE Hash", @@ -134,6 +135,7 @@ var dictionary = { "label.action.create.vm.processing":"Creating VM....", "label.action.create.volume":"Create Volume", "label.action.create.volume.processing":"Creating Volume....", +"label.action.delete.backup.offering":"Delete Backup Offering", "label.action.delete.IP.range":"Delete IP Range", "label.action.delete.IP.range.processing":"Deleting IP Range....", "label.action.delete.ISO":"Delete ISO", @@ -592,6 +594,7 @@ var dictionary = { "label.cpu.limits":"CPU limits", "label.cpu.mhz":"CPU (in MHz)", "label.cpu.utilized":"CPU Utilized", +"label.create.backup":"Start Backup", "label.create.VPN.connection":"Create VPN Connection", "label.create.nfs.secondary.staging.storage":"Create NFS Secondary Staging Store", "label.create.nfs.secondary.staging.store":"Create NFS secondary staging store", @@ -785,6 +788,7 @@ var dictionary = { "label.every":"Every", "label.example":"Example", "label.expunge":"Expunge", +"label.external.id":"External ID", "label.external.link":"External link", "label.extractable":"Extractable", "label.extractable.lower":"extractable", @@ -1066,6 +1070,8 @@ var dictionary = { "label.menu.alerts":"Alerts", "label.menu.all.accounts":"All Accounts", "label.menu.all.instances":"All Instances", +"label.menu.backup":"Backup", +"label.menu.backup.offerings":"Backup Offerings", "label.menu.community.isos":"Community ISOs", "label.menu.community.templates":"Community Templates", "label.menu.configuration":"Configuration", @@ -1808,6 +1814,7 @@ var dictionary = { "label.view.more":"View more", "label.view.secondary.ips":"View secondary IPs", "label.viewing":"Viewing", +"label.virtual.size":"Virtual Size", "label.virtual.appliance":"Virtual Appliance", "label.virtual.appliance.details":"Virtual applicance details", "label.virtual.appliances":"Virtual Appliances", @@ -1842,6 +1849,12 @@ var dictionary = { "label.vm.stop":"Stop", "label.vmfs":"VMFS", "label.vms":"VMs", +"label.backup":"Backups", +"label.backup.offering":"Backup Offering", +"label.backup.offering.assign":"Assign VM to backup offering", +"label.backup.offering.remove":"Remove VM from backup offering", +"label.backup.restore":"Restore VM Backup", +"label.backup.user.driven":"Allow User Driven Backups", "label.vmsnapshot":"VM Snapshots", "label.vmsnapshot.current":"isCurrent", "label.vmsnapshot.memory":"Snapshot memory", @@ -1926,6 +1939,7 @@ var dictionary = { "message.action.cancel.maintenance.mode":"Please confirm that you want to cancel this maintenance.", "message.action.change.service.warning.for.instance":"Your instance must be stopped before attempting to change its current service offering.", "message.action.change.service.warning.for.router":"Your router must be stopped before attempting to change its current service offering.", +"message.action.delete.backup.offering":"Please confirm that you want to delete this backup offering?", "message.action.delete.ISO":"Please confirm that you want to delete this ISO.", "message.action.delete.ISO.for.all.zones":"The ISO is used by all zones. Please confirm that you want to delete it from all zones.", "message.action.delete.cluster":"Please confirm that you want to delete this cluster.", diff --git a/ui/scripts/configuration.js b/ui/scripts/configuration.js index 62a602263f4..641fc549357 100644 --- a/ui/scripts/configuration.js +++ b/ui/scripts/configuration.js @@ -28,7 +28,7 @@ sectionSelect: { preFilter: function(args) { if(isAdmin()) - return ["serviceOfferings", "systemServiceOfferings", "diskOfferings", "networkOfferings", "vpcOfferings"]; + return ["serviceOfferings", "systemServiceOfferings", "diskOfferings", "networkOfferings", "vpcOfferings", "backupOfferings"]; else if(isDomainAdmin()) return ["serviceOfferings", "diskOfferings"]; else @@ -2926,6 +2926,270 @@ } }, + backupOfferings: { + type: 'select', + title: 'label.menu.backup.offerings', + listView: { + id: 'backupOfferings', + label: 'label.menu.backup.offerings', + fields: { + name: { + label: 'label.name', + editable: true + }, + description: { + label: 'label.description' + }, + zonename: { + label: 'label.zone', + } + }, + + actions: { + add: { + label: 'label.import.backup.offering', + createForm: { + title: 'label.import.backup.offering', + fields: { + name: { + label: 'label.name', + validation: { + required: true + } + }, + description: { + label: 'label.description', + validation: { + required: true + } + }, + zoneid: { + label: 'label.zone', + validation: { + required: true + }, + select: function(args) { + $.ajax({ + url: createURL("listZones"), + data: {available: 'true'}, + dataType: "json", + async: true, + success: function(json) { + var items = []; + var zoneObjs = json.listzonesresponse.zone; + $(zoneObjs).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }); + items.sort(function(a, b) { + return a.description.localeCompare(b.description); + }); + items.unshift({ + id: -1, + description: '' + }); + args.response.success({ + data: items + }); + args.$select.change(function() { + var $form = $(this).closest('form'); + var zoneId = $form.find('select#label_zone').val(); + var extSelect = $form.find('select#label_external_id'); + extSelect.empty(); + if (zoneId === -1) { + return; + } + $.ajax({ + url: createURL("listBackupProviderOfferings"), + data: {zoneid: zoneId}, + dataType: "json", + success: function(json) { + var items = []; + var offerings = json.listbackupproviderofferingsresponse.backupoffering; + $(offerings).each(function() { + extSelect.append(new Option(this.name, this.externalid)) + }); + } + }); + }) + } + }); + } + }, + externalid: { + label: 'label.external.id', + select: function(args) { + args.response.success({ + data: [] + }); + } + }, + allowuserdrivenbackups: { + label: 'label.backup.user.driven', + isBoolean: true, + isChecked: true + } + }//end of fields + }, //end of createForm + + action: function(args) { + $.ajax({ + url: createURL('importBackupOffering'), + data: { + name: args.data.name, + description: args.data.description, + zoneid: args.data.zoneid, + externalid: args.data.externalid, + allowuserdrivenbackups: args.data.allowuserdrivenbackups === 'on' + }, + dataType: 'json', + success: function(json) { + var jid = json.importbackupofferingresponse.jobid; + args.response.success({ + _custom: { + jobId: jid, + getActionFilter: function() { + return backupOfferingActionfilter; + } + } + + }); + }, + error: function(data) { + args.response.error(parseXMLHttpResponse(data)); + } + }); + }, + + notification: { + poll: pollAsyncJobResult + }, + + messages: { + notification: function(args) { + return 'label.import.backup.offering'; + } + } + } + }, + + dataProvider: function(args) { + var data = {}; + listViewDataProvider(args, data); + + $.ajax({ + url: createURL('listBackupOfferings'), + data: data, + success: function(json) { + var items = json.listbackupofferingsresponse.backupoffering; + args.response.success({ + data: items + }); + }, + error: function(data) { + args.response.error(parseXMLHttpResponse(data)); + } + }); + }, + + detailView: { + name: 'label.system.backup.offering.details', + actions: { + remove: { + label: 'label.action.delete.backup.offering', + messages: { + confirm: function(args) { + return 'message.action.delete.backup.offering'; + }, + notification: function(args) { + return 'label.action.delete.backup.offering'; + } + }, + action: function(args) { + var data = { + id: args.context.backupOfferings[0].id + }; + $.ajax({ + url: createURL('deleteBackupOffering'), + data: data, + success: function(json) { + args.response.success(); + }, + error: function(data) { + args.response.error(parseXMLHttpResponse(data)); + } + }); + }, + notification: { + poll: function(args) { + args.complete(); + } + } + } + }, + + tabs: { + details: { + title: 'label.details', + + fields: [{ + name: { + label: 'label.name', + isEditable: true, + validation: { + required: true + } + } + }, { + id: { + label: 'label.id' + }, + description: { + label: 'label.description', + isEditable: true, + validation: { + required: true + } + }, + externalid: { + label: 'label.external.id', + }, + allowuserdrivenbackups: { + label: 'label.backup.user.driven' + }, + zoneid: { + label: 'label.zone.id' + }, + created: { + label: 'label.created', + converter: cloudStack.converters.toLocalDate + } + }], + + dataProvider: function(args) { + var data = { + id: args.context.backupOfferings[0].id + }; + $.ajax({ + url: createURL('listBackupOfferings'), + data: data, + success: function(json) { + var item = json.listbackupofferingsresponse.backupoffering[0]; + args.response.success({ + actionFilter: backupOfferingActionfilter, + data: item + }); + } + }); + } + } + } + } + } + }, + networkOfferings: { type: 'select', title: 'label.menu.network.offerings', @@ -5574,6 +5838,13 @@ return allowedActions; }; + var backupOfferingActionfilter = function(args) { + var jsonObj = args.context.item; + var allowedActions = []; + allowedActions.push("remove"); + return allowedActions; + }; + var diskOfferingActionfilter = function(args) { var jsonObj = args.context.item; var allowedActions = []; diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index c6fb671e184..bb32d3a64df 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -795,6 +795,9 @@ }, { path: 'storage.vmsnapshots', label: 'label.snapshots' + }, { + path: 'storage.backups', + label: 'label.backup' }, { path: 'affinityGroups', label: 'label.affinity.groups' @@ -1257,6 +1260,422 @@ poll: pollAsyncJobResult } }, + + createBackup: { + messages: { + confirm: function(args) { + return 'label.create.backup'; + }, + notification: function() { + return 'label.create.backup'; + } + }, + label: 'label.create.backup', + action: function(args) { + var data = { + virtualmachineid: args.context.instances[0].id + }; + $.ajax({ + url: createURL('createBackup'), + data: data, + dataType: 'json', + success: function(json) { + var jid = json.createbackupresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + }, + + configureBackupSchedule: { + label: 'Backup Schedule', + action: { + custom: cloudStack.uiCustom.backupSchedule({ + desc: 'Configure VM backup schedule', + dataProvider: function(args) { + $.ajax({ + url: createURL('listBackupSchedule'), + data: { + virtualmachineid: args.context.instances[0].id + }, + async: true, + dataType: 'json', + success: function(data) { + var schedule = {} + if (data && data.listbackupscheduleresponse && data.listbackupscheduleresponse.backupschedule) { + schedule = data.listbackupscheduleresponse.backupschedule; + schedule.id = schedule.virtualmachineid; + if (schedule.intervaltype == 'HOURLY') { + schedule.type = 0; + schedule.time = schedule.schedule; + } else if (schedule.intervaltype == 'DAILY') { + schedule.type = 1; + schedule.time = schedule.schedule.split(':')[1] + ':' + schedule.schedule.split(':')[0]; + } else if (schedule.intervaltype == 'WEEKLY') { + schedule.type = 2; + schedule.time = schedule.schedule.split(':')[1] + ':' + schedule.schedule.split(':')[0]; + schedule['day-of-week'] = schedule.schedule.split(':')[2]; + } else if (schedule.intervaltype == 'MONTHLY') { + schedule.type = 3; + schedule.time = schedule.schedule.split(':')[1] + ':' + schedule.schedule.split(':')[0]; + schedule['day-of-month'] = schedule.schedule.split(':')[2]; + } + schedule.time = '(' + schedule.intervaltype + ') ' + schedule.time + } + args.response.success({ + data: [schedule] + }); + }, + error: function(data) { + } + }); + }, + actions: { + add: function(args) { + var snap = args.snapshot; + + var data = { + virtualmachineid: args.context.instances[0].id, + intervaltype: snap['snapshot-type'], + timezone: snap.timezone + }; + + var convertTime = function(minute, hour, meridiem, extra) { + var convertedHour = meridiem == 'PM' ? + (hour != 12 ? parseInt(hour) + 12 : 12) : (hour != 12 ? hour : '00'); + var time = minute + ':' + convertedHour; + if (extra) time += ':' + extra; + + return time; + }; + + switch (snap['snapshot-type']) { + case 'hourly': // Hourly + $.extend(data, { + schedule: snap.schedule + }); + break; + + case 'daily': // Daily + $.extend(data, { + schedule: convertTime( + snap['time-minute'], + snap['time-hour'], + snap['time-meridiem'] + ) + }); + break; + + case 'weekly': // Weekly + $.extend(data, { + schedule: convertTime( + snap['time-minute'], + snap['time-hour'], + snap['time-meridiem'], + snap['day-of-week'] + ) + }); + break; + + case 'monthly': // Monthly + $.extend(data, { + schedule: convertTime( + snap['time-minute'], + snap['time-hour'], + snap['time-meridiem'], + snap['day-of-month'] + ) + }); + break; + } + + $.ajax({ + url: createURL('createBackupSchedule'), + data: data, + dataType: 'json', + async: true, + success: function(data) { + var schedule = {} + if (data && data.createbackupscheduleresponse && data.createbackupscheduleresponse.backupschedule) { + schedule = data.createbackupscheduleresponse.backupschedule; + schedule.id = schedule.virtualmachineid; + if (schedule.intervaltype == 'HOURLY') { + schedule.type = 0; + schedule.time = schedule.schedule; + } else if (schedule.intervaltype == 'DAILY') { + schedule.type = 1; + schedule.time = schedule.schedule.split(':')[1] + ':' + schedule.schedule.split(':')[0]; + } else if (schedule.intervaltype == 'WEEKLY') { + schedule.type = 2; + schedule.time = schedule.schedule.split(':')[1] + ':' + schedule.schedule.split(':')[0]; + schedule['day-of-week'] = schedule.schedule.split(':')[2]; + } else if (schedule.intervaltype == 'MONTHLY') { + schedule.type = 3; + schedule.time = schedule.schedule.split(':')[1] + ':' + schedule.schedule.split(':')[0]; + schedule['day-of-month'] = schedule.schedule.split(':')[2]; + } + schedule.time = schedule.time + ' (' + schedule.intervaltype + ')' + } + args.response.success({ + data: schedule + }); + } + }); + }, + remove: function(args) { + console.log(args); + $.ajax({ + url: createURL('deleteBackupSchedule'), + data: { + virtualmachineid: args.context.instances[0].id + }, + dataType: 'json', + async: true, + success: function(data) { + args.response.success(); + } + }); + } + }, + + // Select data + selects: { + schedule: function(args) { + var time = []; + + for (var i = 1; i <= 59; i++) { + time.push({ + id: i, + name: i + }); + } + + args.response.success({ + data: time + }); + }, + timezone: function(args) { + args.response.success({ + data: $.map(timezoneMap, function(value, key) { + return { + id: key, + name: value + }; + }) + }); + }, + 'day-of-week': function(args) { + args.response.success({ + data: [{ + id: 1, + name: 'label.sunday' + }, { + id: 2, + name: 'label.monday' + }, { + id: 3, + name: 'label.tuesday' + }, { + id: 4, + name: 'label.wednesday' + }, { + id: 5, + name: 'label.thursday' + }, { + id: 6, + name: 'label.friday' + }, { + id: 7, + name: 'label.saturday' + }] + }); + }, + + 'day-of-month': function(args) { + var time = []; + + for (var i = 1; i <= 28; i++) { + time.push({ + id: i, + name: i + }); + } + + args.response.success({ + data: time + }); + }, + + 'time-hour': function(args) { + var time = []; + + for (var i = 1; i <= 12; i++) { + time.push({ + id: i, + name: i + }); + } + + args.response.success({ + data: time + }); + }, + + 'time-minute': function(args) { + var time = []; + + for (var i = 0; i <= 59; i++) { + time.push({ + id: i < 10 ? '0' + i : i, + name: i < 10 ? '0' + i : i + }); + } + + args.response.success({ + data: time + }); + }, + + 'time-meridiem': function(args) { + args.response.success({ + data: [{ + id: 'AM', + name: 'AM' + }, { + id: 'PM', + name: 'PM' + }] + }); + } + } + }) + }, + messages: { + notification: function(args) { + return 'Backup Schedule'; + } + } + }, + + assignToBackupOffering: { + messages: { + confirm: function(args) { + return 'label.backup.offering.assign'; + }, + notification: function() { + return 'label.backup.offering.assign'; + } + }, + label: 'label.backup.offering.assign', + createForm: { + title: 'label.backup.offering.assign', + fields: { + backupofferingid: { + label: 'label.backup.offering', + select: function(args) { + var data = { + zoneid: args.context.instances[0].zoneid + }; + $.ajax({ + url: createURL('listBackupOfferings'), + data: data, + async: false, + success: function(json) { + var offerings = json.listbackupofferingsresponse.backupoffering; + var items = [{ + id: -1, + description: '' + }]; + $(offerings).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }); + args.response.success({ + data: items + }); + } + }); + } + } + } + }, + action: function(args) { + var data = { + virtualmachineid: args.context.instances[0].id, + backupofferingid: args.data.backupofferingid + }; + $.ajax({ + url: createURL('assignVirtualMachineToBackupOffering'), + data: data, + dataType: 'json', + success: function(json) { + var jid = json.assignvirtualmachinetobackupofferingresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + }, + + removeFromBackupOffering: { + messages: { + confirm: function(args) { + return 'label.backup.offering.remove'; + }, + notification: function() { + return 'label.backup.offering.remove'; + } + }, + label: 'label.backup.offering.remove', + createForm: { + title: 'label.backup.offering.remove', + fields: { + forced: { + label: 'force.remove', + isBoolean: true, + isChecked: false + } + } + }, + action: function(args) { + var data = { + virtualmachineid: args.context.instances[0].id, + forced: args.data.forced === "on" + }; + $.ajax({ + url: createURL('removeVirtualMachineFromBackupOffering'), + data: data, + dataType: 'json', + success: function(json) { + var jid = json.removevirtualmachinefrombackupofferingresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + }, + destroy: vmDestroyAction(), expunge: { label: 'label.action.expunge.instance', @@ -2884,6 +3303,9 @@ keypair: { label: 'label.ssh.key.pair' }, + backupofferingname: { + label: 'label.backup.offering' + }, domain: { label: 'label.domain' }, @@ -3735,6 +4157,13 @@ allowedActions.push("expunge"); } } + if (jsonObj.backupofferingid) { + allowedActions.push("createBackup"); + allowedActions.push("configureBackupSchedule"); + allowedActions.push("removeFromBackupOffering"); + } else { + allowedActions.push("assignToBackupOffering"); + } if (jsonObj.state == 'Starting' || jsonObj.state == 'Stopping' || jsonObj.state == 'Migrating') { allowedActions.push("viewConsole"); diff --git a/ui/scripts/storage.js b/ui/scripts/storage.js index a30ac60f78d..aa355ef5afa 100644 --- a/ui/scripts/storage.js +++ b/ui/scripts/storage.js @@ -2773,6 +2773,328 @@ } //detailview end } + }, + + /** + * Backups + */ + backups: { + type: 'select', + title: 'label.backup', + listView: { + id: 'backups', + isMaximized: true, + fields: { + virtualmachinename: { + label: 'label.vm.name' + }, + status: { + label: 'label.state', + indicator: { + 'BackedUp': 'on', + 'Failed': 'off', + 'Error': 'off' + } + }, + type: { + label: 'label.type' + }, + created: { + label: 'label.date' + }, + account: { + label: 'label.account' + }, + zone: { + label: 'label.zone' + } + }, + + dataProvider: function(args) { + var data = { + listAll: true + }; + listViewDataProvider(args, data); + + if (args.context != null) { + if ("instances" in args.context) { + $.extend(data, { + virtualmachineid: args.context.instances[0].id + }); + } + } + + $.ajax({ + url: createURL('listBackups'), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jsonObj; + jsonObj = json.listbackupsresponse.backup; + args.response.success({ + actionFilter: backupActionfilter, + data: jsonObj + }); + } + }); + }, + //dataProvider end + detailView: { + tabs: { + details: { + title: 'label.details', + fields: { + id: { + label: 'label.id' + }, + virtualmachinename: { + label: 'label.vm.name' + }, + virtualmachineid: { + label: 'label.vm.id' + }, + status: { + label: 'label.state' + }, + externalid: { + label: 'label.external.id' + }, + type: { + label: 'label.type' + }, + size: { + label: 'label.size' + }, + virtualsize: { + label: 'label.virtual.size' + }, + volumes: { + label: 'label.volumes' + }, + account: { + label: 'label.account' + }, + domain: { + label: 'label.domain' + }, + zone: { + label: 'label.zone' + }, + created: { + label: 'label.date' + } + }, + dataProvider: function(args) { + $.ajax({ + url: createURL("listBackups&id=" + args.context.backups[0].id), + dataType: "json", + async: true, + success: function(json) { + var jsonObj; + jsonObj = json.listbackupsresponse.backup[0]; + args.response.success({ + actionFilter: backupActionfilter, + data: jsonObj + }); + } + }); + } + } + }, + actions: { + remove: { + label: 'Delete Backup', + messages: { + confirm: function(args) { + return 'Are you sure you want to delete the backup?'; + }, + notification: function(args) { + return 'Delete Backup'; + } + }, + action: function(args) { + $.ajax({ + url: createURL("deleteBackup&id=" + args.context.backups[0].id), + dataType: "json", + async: true, + success: function(json) { + var jid = json.deletebackupresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + }, + + restoreBackup: { + label: 'label.backup.restore', + messages: { + confirm: function(args) { + return 'Please confirm that you want to restore the vm backup?'; + }, + notification: function(args) { + return 'label.backup.restore'; + } + }, + action: function(args) { + var data = { + id: args.context.backups[0].id + }; + $.ajax({ + url: createURL("restoreBackup"), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jid = json.restorebackupresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + + }, + notification: { + poll: pollAsyncJobResult + } + }, + + restoreBackupVolume: { + label: 'Restore and Attach Backup Volume', + messages: { + confirm: function(args) { + return 'Please confirm that you want to restore and attach the volume from the backup?'; + }, + notification: function(args) { + return 'Restore and Attach Backup Volume'; + } + }, + createForm: { + title: 'Restore and Attach Backup Volume', + desc: 'Please select the volume you want to restore and attach to the VM.', + fields: { + volume: { + label: 'label.volume', + validation: { + required: true + }, + select: function(args) { + var volumes = JSON.parse(args.context.backups[0].volumes); + var items = []; + $(volumes).each(function() { + items.push({ + id: this.uuid, + description: '(' + this.type + ') ' + this.uuid + }); + }); + args.response.success({ + data: items + }); + } + }, + virtualmachine: { + label: 'label.virtual.machine', + validation: { + required: true + }, + select: function(args) { + $.ajax({ + url: createURL("listVirtualMachines"), + dataType: "json", + async: true, + success: function(json) { + var vms = json.listvirtualmachinesresponse.virtualmachine; + var items = []; + $(vms).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }); + args.response.success({ + data: items + }); + + } + }); + } + } + } + }, + action: function(args) { + console.log(args); + var data = { + backupid: args.context.backups[0].id, + volumeid: args.data.volume, + virtualmachineid: args.data.virtualmachine + }; + $.ajax({ + url: createURL("restoreVolumeFromBackupAndAttachToVM"), + data: data, + dataType: "json", + async: true, + success: function(json) { + var jid = json.restorevolumefrombackupandattachtovmresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + + }, + notification: { + poll: pollAsyncJobResult + } + }, + + removeBackupChain: { + label: 'Delete Backup Chain', + messages: { + confirm: function(args) { + return 'Are you sure you want to remove VM from backup offering and delete the backup chain?'; + }, + notification: function(args) { + return 'Delete Backup Chain'; + } + }, + action: function(args) { + $.ajax({ + url: createURL("removeVirtualMachineFromBackupOffering"), + data: { + virtualmachineid: args.context.backups[0].virtualmachineid, + forced: true + }, + dataType: "json", + async: true, + success: function(json) { + var jid = json.removevirtualmachinefrombackupofferingresponse.jobid; + args.response.success({ + _custom: { + jobId: jid + } + }); + } + }); + }, + notification: { + poll: pollAsyncJobResult + } + } + } + } + //detailview end + } } } }; @@ -2903,4 +3225,22 @@ return allowedActions; } + var backupActionfilter = cloudStack.actionFilter.backupActionfilter = function(args) { + var jsonObj = args.context.item; + + if (jsonObj.state == 'Destroyed') { + return []; + } + + var allowedActions = []; + allowedActions.push("remove"); + allowedActions.push("restoreBackup"); + allowedActions.push("restoreBackupVolume"); + allowedActions.push("removeBackupChain"); + + return allowedActions; + }; + + + })(cloudStack); diff --git a/ui/scripts/ui-custom/backupSchedule.js b/ui/scripts/ui-custom/backupSchedule.js new file mode 100644 index 00000000000..56468989bd6 --- /dev/null +++ b/ui/scripts/ui-custom/backupSchedule.js @@ -0,0 +1,181 @@ +// 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.backupSchedule = function(args) { + var desc = args.desc; + var selects = args.selects; + var actions = args.actions; + var dataProvider = args.dataProvider; + + return function(args) { + var $backups = $('#template').find('.recurring-snapshots').clone(); + var context = args.context; + + // Update labels + $backups.find('.forms ul li.hourly a').html(_l('label.hourly')); + $backups.find('.forms ul li.daily a').html(_l('label.daily')); + $backups.find('.forms ul li.weekly a').html(_l('label.weekly')); + $backups.find('.forms ul li.monthly a').html(_l('label.monthly')); + $backups.find('.field.timezone .name').html(_l('label.timezone')); + $backups.find('.field.time .name').html(_l('label.time')); + $backups.find('.field.time .value label').html(_l('label.minute.past.hour')); + $backups.find('.field.maxsnaps').hide(); + $backups.find('.add-snapshot-action.add').html(_l('label.configure')); + + $backups.find('.desc').html(_l(desc)); + $backups.find('.forms').tabs(); + + $backups.find('form select').each(function() { + var $select = $(this); + var selectData = selects[$select.attr('name')]; + + if (selectData) { + selectData({ + response: { + success: function(args) { + $(args.data).each(function() { + var $option = $('