shwstppr commented on code in PR #9208: URL: https://github.com/apache/cloudstack/pull/9208#discussion_r1705516454
########## api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java: ########## @@ -69,6 +69,8 @@ public void execute() { response.setInstancesStatsUserOnly((Boolean) capabilities.get(ApiConstants.INSTANCES_STATS_USER_ONLY)); response.setInstancesDisksStatsRetentionEnabled((Boolean) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_ENABLED)); response.setInstancesDisksStatsRetentionTime((Integer) capabilities.get(ApiConstants.INSTANCES_DISKS_STATS_RETENTION_TIME)); + response.setStorageFsVmMinCpuCount((Integer)capabilities.get("storageFsVmMinCpuCount")); Review Comment: Can we use same constants that are used in ApiCmd classes? ########## ui/src/config/section/compute.js: ########## @@ -108,6 +108,7 @@ export default { docHelp: 'adminguide/virtual_machines.html#changing-the-vm-name-os-or-group', dataView: true, popup: true, + show: (record) => { return record.vmtype !== 'storagefsvm' }, Review Comment: Should we hide the options or should we show some warning in the dialogs? I don't have a strong preference though ########## ui/src/views/storage/CreateFileShare.vue: ########## @@ -0,0 +1,492 @@ +// 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. + +<template> + <a-spin :spinning="loading"> + <div v-if="!isNormalUserOrProject"> + <ownership-selection @fetch-owner="fetchOwnerOptions" /> + </div> + <a-form + class="form" + :ref="formRef" + :model="form" + :rules="rules" + layout="vertical" + @finish="handleSubmit" + v-ctrl-enter="handleSubmit" + > + <a-form-item name="name" ref="name" :label="$t('label.name')"> + <a-input v-model:value="form.name" v-focus="true" /> + </a-form-item> + <a-form-item name="description" ref="description" :label="$t('label.description')"> + <a-input v-model:value="form.description" /> + </a-form-item> + <a-form-item ref="zoneid" name="zoneid"> + <template #label> + <tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid.description"/> + </template> + <a-select + v-model:value="form.zoneid" + :loading="zoneLoading" + @change="zone => handleZoneChange(id)" + :placeholder="apiParams.zoneid.description" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" > + <a-select-option + v-for="(zone, index) in zones" + :value="zone.id" + :key="index" + :label="zone.name"> + <span> + <resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/> + <global-outlined v-else style="margin-right: 5px"/> + {{ zone.name }} + </span> + </a-select-option> + </a-select> + </a-form-item> + <a-form-item ref="provider" name="provider"> + <template #label> + <tooltip-label :title="$t('label.provider')" :tooltip="apiParams.provider.description"/> + </template> + <a-select + v-model:value="form.provider" + :loading="providersLoading" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" > + <a-select-option + v-for="(provider, index) in providers" + :value="provider.name" + :key="index" + :label="provider.name"> + </a-select-option> + </a-select> + </a-form-item> + <a-form-item + name="format" + ref="format" + :placeholder="apiParams.format.description" > + <a-input v-model:value="form.format" /> + <template #label> + <tooltip-label :title="$t('label.format')" :tooltip="apiParams.format.description"/> + </template> + </a-form-item> + <a-form-item ref="networkid" name="networkid"> + <template #label> + <tooltip-label :title="$t('label.networkid')" :tooltip="apiParams.networkid.description || 'Network'"/> + </template> + <a-select + v-model:value="form.networkid" + :loading="networkLoading" + :placeholder="apiParams.networkid.description || $t('label.networkid')" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" > + <a-select-option + v-for="(network, index) in networks" + :value="network.id" + :key="index" + :label="network.name"> {{ network.name }} + </a-select-option> + </a-select> + </a-form-item> + <a-form-item ref="serviceofferingid" name="serviceofferingid"> + <template #label> + <tooltip-label :title="$t('label.serviceofferingid')" :tooltip="apiParams.serviceofferingid.description || 'Service Offering'"/> + </template> + <a-select + v-model:value="form.serviceofferingid" + :loading="serviceofferingLoading" + :placeholder="apiParams.serviceofferingid.description || $t('label.serviceofferingid')" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" > + <a-select-option + v-for="(serviceoffering, index) in serviceofferings" + :value="serviceoffering.id" + :key="index" + :label="serviceoffering.displaytext || serviceoffering.name"> + {{ serviceoffering.displaytext || serviceoffering.name }} + </a-select-option> + </a-select> + </a-form-item> + <a-form-item ref="diskofferingid" name="diskofferingid"> + <template #label> + <tooltip-label :title="$t('label.diskofferingid')" :tooltip="apiParams.diskofferingid.description || 'Disk Offering'"/> + </template> + <a-select + v-model:value="form.diskofferingid" + :loading="diskofferingLoading" + @change="id => handleDiskOfferingChange(id)" + :placeholder="apiParams.diskofferingid.description || $t('label.diskofferingid')" + showSearch + optionFilterProp="label" + :filterOption="(input, option) => { + return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 + }" > + <a-select-option + v-for="(diskoffering, index) in diskofferings" + :value="diskoffering.id" + :key="index" + :label="diskoffering.displaytext || diskoffering.name"> + {{ diskoffering.displaytext || diskoffering.name }} + </a-select-option> + </a-select> + </a-form-item> + <span v-if="customDiskOffering"> + <a-form-item ref="size" name="size"> + <template #label> + <tooltip-label :title="$t('label.sizegb')" :tooltip="apiParams.size.description"/> + </template> + <a-input + v-model:value="form.size" + :placeholder="apiParams.size.description"/> + </a-form-item> + </span> + <span v-if="isCustomizedDiskIOps"> + <a-form-item ref="miniops" name="miniops"> + <template #label> + <tooltip-label :title="$t('label.miniops')" :tooltip="apiParams.miniops.description"/> + </template> + <a-input + v-model:value="form.miniops" + :placeholder="apiParams.miniops.description"/> + </a-form-item> + <a-form-item ref="maxiops" name="maxiops"> + <template #label> + <tooltip-label :title="$t('label.maxiops')" :tooltip="apiParams.maxiops.description"/> + </template> + <a-input + v-model:value="form.maxiops" + :placeholder="apiParams.maxiops.description"/> + </a-form-item> + </span> + <div :span="24" class="action-button"> + <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button> + <a-button type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button> + </div> + </a-form> + </a-spin> +</template> +<script> + +import { ref, reactive, toRaw } from 'vue' +import { api } from '@/api' +import { mixinForm } from '@/utils/mixin' +import ResourceIcon from '@/components/view/ResourceIcon' +import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue' +import TooltipLabel from '@/components/widgets/TooltipLabel' +import store from '@/store' + +export default { + name: 'CreateFileShare', + mixins: [mixinForm], + props: { + resource: { + type: Object, + required: true + } + }, + components: { + OwnershipSelection, + ResourceIcon, + TooltipLabel + }, + inject: ['parentFetchData'], + data () { + return { + owner: { + projectid: store.getters.project?.id, + domainid: store.getters.project?.id ? null : store.getters.userInfo.domainid, + account: store.getters.project?.id ? null : store.getters.userInfo.account + }, + loading: false, + zones: [], + zoneLoading: false, + providers: [], + providersLoading: false, + configLoading: false, + networks: [], + networkLoading: false, + serviceofferings: [], + serviceofferingLoading: false, + diskofferings: [], + diskofferingLoading: false, + customDiskOffering: false, + isCustomizedDiskIOps: false + } + }, + computed: { + isNormalUserOrProject () { + return ['User'].includes(this.$store.getters.userInfo.roletype) || store.getters.project?.id + } + }, + beforeCreate () { + this.apiParams = this.$getApiParams('createFileShare') + }, + created () { + this.initForm() + this.fetchData() + this.form.format = 'XFS' + }, + methods: { + initForm () { + this.formRef = ref() + this.form = reactive({ + }) + this.rules = reactive({ + zoneid: [{ required: true, message: this.$t('message.error.zone') }], + name: [{ required: true, message: this.$t('label.required') }], + networkid: [{ required: true, message: this.$t('label.required') }], + serviceofferingid: [{ required: true, message: this.$t('label.required') }], + diskofferingid: [{ required: true, message: this.$t('label.required') }], + size: [{ required: true, message: this.$t('message.error.custom.disk.size') }], + miniops: [{ + validator: async (rule, value) => { + if (value && (isNaN(value) || value <= 0)) { + return Promise.reject(this.$t('message.error.number')) + } + return Promise.resolve() + } + }], + maxiops: [{ + validator: async (rule, value) => { + if (value && (isNaN(value) || value <= 0)) { + return Promise.reject(this.$t('message.error.number')) + } + return Promise.resolve() + } + }] + }) + }, + arrayHasItems (array) { + return array !== null && array !== undefined && Array.isArray(array) && array.length > 0 + }, + fetchOwnerOptions (OwnerOptions) { + this.owner = {} + console.log('fetching owner') + if (OwnerOptions.selectedAccountType === this.$t('label.account')) { + if (!OwnerOptions.selectedAccount) { + return + } + console.log('fetched account') + this.owner.account = OwnerOptions.selectedAccount + this.owner.domainid = OwnerOptions.selectedDomain + } else if (OwnerOptions.selectedAccountType === this.$t('label.project')) { + if (!OwnerOptions.selectedProject) { + return + } + console.log('fetched project') + this.owner.projectid = OwnerOptions.selectedProject + } + console.log('fetched owner') + this.fetchData() + }, + fetchData () { + this.fetchZones() + this.fetchFileShareProviders() + }, + fetchZones () { + this.zoneLoading = true + const params = { showicon: true } + api('listZones', params).then(json => { + var listZones = json.listzonesresponse.zone + if (listZones) { + listZones = listZones.filter(x => x.allocationstate === 'Enabled') + this.zones = this.zones.concat(listZones) + } + }).finally(() => { + this.zoneLoading = false + if (this.arrayHasItems(this.zones)) { + this.form.zoneid = this.zones[0].id + this.handleZoneChange(this.zones[0]) + } + }) + }, + handleZoneChange (zone) { + this.selectedZone = zone + this.fetchServiceOfferings() + this.fetchDiskOfferings() + this.fetchNetworks() + }, + fetchFileShareProviders (id) { + this.providersLoading = true + api('listFileShareProviders').then(json => { + this.providers = json.listfileshareprovidersresponse.fileshareprovider || [] + this.form.provider = this.providers[0].name || '' + }).finally(() => { + this.providersLoading = false + }) + }, + fetchCapabilities (id) { Review Comment: we already do listCapabilities call during user login, I guess we can use that Search for `$store.getters.features.` in UI code ########## api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java: ########## @@ -124,6 +124,14 @@ public class CapabilitiesResponse extends BaseResponse { @Param(description = "the retention time for Instances disks stats", since = "4.18.0") private Integer instancesDisksStatsRetentionTime; + @SerializedName("storagefsvmmincpucount") Review Comment: Can these constants be moved to ApiConstants class? ########## server/src/main/java/org/apache/cloudstack/storage/fileshare/FileShareServiceImpl.java: ########## @@ -0,0 +1,687 @@ +// 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.storage.fileshare; + +import static org.apache.cloudstack.storage.fileshare.FileShare.FileShareCleanupDelay; +import static org.apache.cloudstack.storage.fileshare.FileShare.FileShareCleanupInterval; +import static org.apache.cloudstack.storage.fileshare.FileShare.FileShareFeatureEnabled; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import com.cloud.configuration.ConfigurationManager; +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.org.Grouping; +import com.cloud.projects.Project; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.EntityManager; +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.fsm.NoTransitionException; +import com.cloud.utils.fsm.StateMachine2; + +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.user.storage.fileshare.ChangeFileShareDiskOfferingCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ChangeFileShareServiceOfferingCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.CreateFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ExpungeFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ListFileShareProvidersCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ListFileSharesCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.DestroyFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.RecoverFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.RestartFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.StartFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.StopFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.UpdateFileShareCmd; +import org.apache.cloudstack.api.response.FileShareResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.storage.fileshare.dao.FileShareDao; +import org.apache.cloudstack.storage.fileshare.FileShare.Event; +import org.apache.cloudstack.storage.fileshare.FileShare.State; +import org.apache.cloudstack.storage.fileshare.query.dao.FileShareJoinDao; +import org.apache.cloudstack.storage.fileshare.query.vo.FileShareJoinVO; + +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; + +public class FileShareServiceImpl extends ManagerBase implements FileShareService, Configurable { + + @Inject + private AccountManager accountMgr; + + @Inject + private EntityManager entityMgr; + + @Inject + private ConfigurationManager configMgr; + + @Inject + private VolumeApiService volumeApiService; + + @Inject + private FileShareDao fileShareDao; + + @Inject + private FileShareJoinDao fileShareJoinDao; + + @Inject + private DiskOfferingDao diskOfferingDao; + + @Inject + ConfigurationDao configDao; + + @Inject + VolumeDao volumeDao; + + protected List<FileShareProvider> fileShareProviders; + + private Map<String, FileShareProvider> fileShareProviderMap = new HashMap<>(); + + private final StateMachine2<State, Event, FileShare> fileShareStateMachine; + + ScheduledExecutorService _executor = null; + + public FileShareServiceImpl() { + this.fileShareStateMachine = State.getStateMachine(); + } + + @Override + public boolean start() { + fileShareProviderMap.clear(); + for (final FileShareProvider provider : fileShareProviders) { + fileShareProviderMap.put(provider.getName(), provider); + provider.configure(); + } + _executor.scheduleWithFixedDelay(new FileShareGarbageCollector(), FileShareCleanupInterval.value(), FileShareCleanupInterval.value(), TimeUnit.SECONDS); + return true; + } + + public boolean stop() { + _executor.shutdown(); + return true; + } + + @Override + public List<FileShareProvider> getFileShareProviders() { + return fileShareProviders; + } + + @Override + public boolean stateTransitTo(FileShare fileShare, Event event) { + try { + return fileShareStateMachine.transitTo(fileShare, event, null, fileShareDao); + } catch (NoTransitionException e) { + logger.debug(String.format("Failed during event % for File Share %s [%s] due to exception %", + event.toString(), fileShare.getName(), fileShare.getId(), e)); + return false; + } + } + + @Override + public void setFileShareProviders(List<FileShareProvider> fileShareProviders) { + this.fileShareProviders = fileShareProviders; + } + + @Override + public FileShareProvider getFileShareProvider(String fileShareProviderName) { + if (fileShareProviderMap.containsKey(fileShareProviderName)) { + return fileShareProviderMap.get(fileShareProviderName); + } + throw new CloudRuntimeException("Invalid file share provider name!"); + } + + @Override + public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException { + Map<String, String> configs = configDao.getConfiguration("management-server", params); + String workers = configs.get("expunge.workers"); + int wrks = NumbersUtil.parseInt(workers, 10); Review Comment: should this be configurable as well? ########## server/src/main/java/org/apache/cloudstack/storage/fileshare/FileShareServiceImpl.java: ########## @@ -0,0 +1,687 @@ +// 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.storage.fileshare; + +import static org.apache.cloudstack.storage.fileshare.FileShare.FileShareCleanupDelay; +import static org.apache.cloudstack.storage.fileshare.FileShare.FileShareCleanupInterval; +import static org.apache.cloudstack.storage.fileshare.FileShare.FileShareFeatureEnabled; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import com.cloud.configuration.ConfigurationManager; +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.org.Grouping; +import com.cloud.projects.Project; +import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.VolumeApiService; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.DiskOfferingDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.EntityManager; +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.fsm.NoTransitionException; +import com.cloud.utils.fsm.StateMachine2; + +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.user.storage.fileshare.ChangeFileShareDiskOfferingCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ChangeFileShareServiceOfferingCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.CreateFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ExpungeFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ListFileShareProvidersCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.ListFileSharesCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.DestroyFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.RecoverFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.RestartFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.StartFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.StopFileShareCmd; +import org.apache.cloudstack.api.command.user.storage.fileshare.UpdateFileShareCmd; +import org.apache.cloudstack.api.response.FileShareResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.storage.fileshare.dao.FileShareDao; +import org.apache.cloudstack.storage.fileshare.FileShare.Event; +import org.apache.cloudstack.storage.fileshare.FileShare.State; +import org.apache.cloudstack.storage.fileshare.query.dao.FileShareJoinDao; +import org.apache.cloudstack.storage.fileshare.query.vo.FileShareJoinVO; + +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; + +public class FileShareServiceImpl extends ManagerBase implements FileShareService, Configurable { + + @Inject + private AccountManager accountMgr; + + @Inject + private EntityManager entityMgr; + + @Inject + private ConfigurationManager configMgr; + + @Inject + private VolumeApiService volumeApiService; + + @Inject + private FileShareDao fileShareDao; + + @Inject + private FileShareJoinDao fileShareJoinDao; + + @Inject + private DiskOfferingDao diskOfferingDao; + + @Inject + ConfigurationDao configDao; + + @Inject + VolumeDao volumeDao; + + protected List<FileShareProvider> fileShareProviders; + + private Map<String, FileShareProvider> fileShareProviderMap = new HashMap<>(); + + private final StateMachine2<State, Event, FileShare> fileShareStateMachine; + + ScheduledExecutorService _executor = null; + + public FileShareServiceImpl() { + this.fileShareStateMachine = State.getStateMachine(); + } + + @Override + public boolean start() { + fileShareProviderMap.clear(); + for (final FileShareProvider provider : fileShareProviders) { + fileShareProviderMap.put(provider.getName(), provider); + provider.configure(); + } + _executor.scheduleWithFixedDelay(new FileShareGarbageCollector(), FileShareCleanupInterval.value(), FileShareCleanupInterval.value(), TimeUnit.SECONDS); + return true; + } + + public boolean stop() { + _executor.shutdown(); + return true; + } + + @Override + public List<FileShareProvider> getFileShareProviders() { + return fileShareProviders; + } + + @Override + public boolean stateTransitTo(FileShare fileShare, Event event) { + try { + return fileShareStateMachine.transitTo(fileShare, event, null, fileShareDao); + } catch (NoTransitionException e) { + logger.debug(String.format("Failed during event % for File Share %s [%s] due to exception %", + event.toString(), fileShare.getName(), fileShare.getId(), e)); + return false; + } + } + + @Override + public void setFileShareProviders(List<FileShareProvider> fileShareProviders) { + this.fileShareProviders = fileShareProviders; + } + + @Override + public FileShareProvider getFileShareProvider(String fileShareProviderName) { + if (fileShareProviderMap.containsKey(fileShareProviderName)) { + return fileShareProviderMap.get(fileShareProviderName); + } + throw new CloudRuntimeException("Invalid file share provider name!"); + } + + @Override + public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException { + Map<String, String> configs = configDao.getConfiguration("management-server", params); + String workers = configs.get("expunge.workers"); + int wrks = NumbersUtil.parseInt(workers, 10); + _executor = Executors.newScheduledThreadPool(wrks, new NamedThreadFactory("FileShare-Scavenger")); + return true; + } + + @Override + public List<Class<?>> getCommands() { + final List<Class<?>> cmdList = new ArrayList<>(); + if (FileShareFeatureEnabled.value() == true) { + cmdList.add(ListFileShareProvidersCmd.class); + cmdList.add(CreateFileShareCmd.class); + cmdList.add(ListFileSharesCmd.class); + cmdList.add(UpdateFileShareCmd.class); + cmdList.add(DestroyFileShareCmd.class); + cmdList.add(RestartFileShareCmd.class); + cmdList.add(StartFileShareCmd.class); + cmdList.add(StopFileShareCmd.class); + cmdList.add(ChangeFileShareDiskOfferingCmd.class); + cmdList.add(ChangeFileShareServiceOfferingCmd.class); + cmdList.add(RecoverFileShareCmd.class); + cmdList.add(ExpungeFileShareCmd.class); + } + return cmdList; + } + + private DataCenter validateAndGetZone(Long zoneId) { + DataCenter zone = entityMgr.findById(DataCenter.class, zoneId); + if (zone == null) { + throw new InvalidParameterValueException("Unable to find zone by ID: " + zoneId); + } + if (zone.getAllocationState() == Grouping.AllocationState.Disabled) { + throw new PermissionDeniedException(String.format("Cannot perform this operation, zone ID: %s is currently disabled", zone.getUuid())); + } + return zone; + } + + private DiskOfferingVO validateAndGetDiskOffering(Long diskOfferingId, Long size, Long minIops, Long maxIops, DataCenter zone) { + Account caller = CallContext.current().getCallingAccount(); + DiskOfferingVO diskOffering = diskOfferingDao.findById(diskOfferingId); + configMgr.checkDiskOfferingAccess(caller, diskOffering, zone); + + if (!diskOffering.isCustomized() && size != null) { + throw new InvalidParameterValueException("Size provided with a non-custom disk offering"); + } + if ((diskOffering.isCustomizedIops() == null || diskOffering.isCustomizedIops() == false) && (minIops != null || maxIops != null)) { + throw new InvalidParameterValueException("Iops provided with a non-custom-iops disk offering"); + } + return diskOffering; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_FILESHARE_CREATE, eventDescription = "creating fileshare", create = true) + public FileShare createFileShare(CreateFileShareCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + Account caller = CallContext.current().getCallingAccount(); + + long ownerId = cmd.getEntityOwnerId(); + Account owner = accountMgr.getActiveAccountById(ownerId); + accountMgr.checkAccess(caller, null, true, accountMgr.getActiveAccountById(ownerId)); + DataCenter zone = validateAndGetZone(cmd.getZoneId()); + + Long diskOfferingId = cmd.getDiskOfferingId(); + Long size = cmd.getSize(); + Long minIops = cmd.getMinIops(); + Long maxIops = cmd.getMaxIops(); + DiskOfferingVO diskOffering = validateAndGetDiskOffering(diskOfferingId, size, minIops, maxIops, zone); + + FileShareProvider provider = getFileShareProvider(cmd.getFileShareProviderName()); + FileShareLifeCycle lifeCycle = provider.getFileShareLifeCycle(); + lifeCycle.checkPrerequisites(zone, cmd.getServiceOfferingId()); + + FileShare.FileSystemType fsType; + try { + fsType = FileShare.FileSystemType.valueOf(cmd.getFsFormat().toUpperCase()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException("Invalid File system format specified. Supported formats are EXT4 and XFS"); + } + + FileShareVO fileShare = new FileShareVO(cmd.getName(), cmd.getDescription(),owner.getDomainId(), + ownerId, cmd.getZoneId(), cmd.getFileShareProviderName(), FileShare.Protocol.NFS, + fsType, cmd.getServiceOfferingId()); + fileShareDao.persist(fileShare); + + fileShare = fileShareDao.findById(fileShare.getId()); + Pair<Long, Long> result = null; + try { + result = lifeCycle.commitFileShare(fileShare, cmd.getNetworkId(), diskOfferingId, size, minIops, maxIops); + } catch (Exception ex) { + stateTransitTo(fileShare, Event.OperationFailed); + throw ex; + } + fileShare.setVolumeId(result.first()); + fileShare.setVmId(result.second()); + fileShareDao.update(fileShare.getId(), fileShare); + stateTransitTo(fileShare, Event.OperationSucceeded); + return fileShare; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_FILESHARE_START, eventDescription = "Starting fileshare", async = true) + public FileShare startFileShare(Long fileShareId) throws OperationTimedoutException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + FileShareVO fileShare = fileShareDao.findById(fileShareId); + + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, false, fileShare); + Set<State> validStates = new HashSet<>(List.of(State.Stopped, State.Detached)); + if (!validStates.contains(fileShare.getState())) { + throw new InvalidParameterValueException("File share can be started only if it is in the " + validStates.toString() + " states"); + } + + FileShareProvider provider = getFileShareProvider(fileShare.getFsProviderName()); + FileShareLifeCycle lifeCycle = provider.getFileShareLifeCycle(); + + if (fileShare.getState().equals(State.Detached)) { + Pair<Boolean, Long> result = lifeCycle.reDeployFileShare(fileShare); + if (result.first() == true) { + fileShare.setVmId(result.second()); + fileShareDao.update(fileShare.getId(), fileShare); + stateTransitTo(fileShare, Event.OperationSucceeded); + } else { + stateTransitTo(fileShare, Event.OperationFailed); + return null; + } + } + + stateTransitTo(fileShare, Event.StartRequested); + try { + lifeCycle.startFileShare(fileShare); + } catch (Exception ex) { + stateTransitTo(fileShare, Event.OperationFailed); + throw ex; + } + stateTransitTo(fileShare, Event.OperationSucceeded); + fileShare = fileShareDao.findById(fileShareId); + return fileShare; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_FILESHARE_STOP, eventDescription = "Stopping fileshare", async = true) + public FileShare stopFileShare(Long fileShareId, Boolean forced) { + FileShareVO fileShare = fileShareDao.findById(fileShareId); + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, false, fileShare); + Set<State> validStates = new HashSet<>(List.of(State.Ready)); + if (!validStates.contains(fileShare.getState())) { + throw new InvalidParameterValueException("File share can be stopped only if it is in the " + State.Ready + " state"); + } + + stateTransitTo(fileShare, Event.StopRequested); + FileShareProvider provider = getFileShareProvider(fileShare.getFsProviderName()); + FileShareLifeCycle lifeCycle = provider.getFileShareLifeCycle(); + try { + lifeCycle.stopFileShare(fileShare, forced); + } catch (Exception e) { + stateTransitTo(fileShare, Event.OperationFailed); + throw e; + } + stateTransitTo(fileShare, Event.OperationSucceeded); + return fileShare; + } + + private FileShareVO reDeployFileShare(FileShareVO fileShare, Boolean startVm) throws OperationTimedoutException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + FileShareProvider provider = getFileShareProvider(fileShare.getFsProviderName()); + FileShareLifeCycle lifeCycle = provider.getFileShareLifeCycle(); + DataCenter zone = validateAndGetZone(fileShare.getDataCenterId()); + lifeCycle.checkPrerequisites(zone, fileShare.getServiceOfferingId()); + + if (!fileShare.getState().equals(State.Stopped)) { + stopFileShare(fileShare.getId(), false); + } + + Pair<Boolean, Long> result = lifeCycle.reDeployFileShare(fileShare); + if (result.first() == true) { + fileShare.setVmId(result.second()); + fileShareDao.update(fileShare.getId(), fileShare); + if (startVm) { + startFileShare(fileShare.getId()); + } + return fileShare; + } else { + stateTransitTo(fileShare, Event.Detach); + logger.error("Redeploy failed for fileshare " + fileShare.toString() + ". File share is left in detached state."); + return null; + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_FILESHARE_RESTART, eventDescription = "Restarting fileshare", async = true) + public FileShare restartFileShare(Long fileShareId, boolean cleanup) throws OperationTimedoutException, ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + FileShareVO fileShare = fileShareDao.findById(fileShareId); + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, false, fileShare); + + Set<State> validStates = new HashSet<>(List.of(State.Ready, State.Stopped)); + if (!validStates.contains(fileShare.getState())) { + throw new InvalidParameterValueException("Stop file share can be done only if the file share is in " + validStates.toString() + " states"); + } + + if (cleanup == false) { + if (!fileShare.getState().equals(State.Stopped)) { + stopFileShare(fileShare.getId(), false); + } + return startFileShare(fileShare.getId()); + } else { + return reDeployFileShare(fileShare, true); + } + } + + private Pair<List<Long>, Integer> searchForFileSharesIdsAndCount(ListFileSharesCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + List<Long> permittedAccounts = new ArrayList<>(); + + Long id = cmd.getId(); + String name = cmd.getName(); + Long networkId = cmd.getNetworkId(); + Long diskOfferingId = cmd.getDiskOfferingId(); + Long serviceOfferingId = cmd.getServiceOfferingId(); + String keyword = cmd.getKeyword(); + Long startIndex = cmd.getStartIndex(); + Long pageSize = cmd.getPageSizeVal(); + Long zoneId = cmd.getZoneId(); + String accountName = cmd.getAccountName(); + Long domainId = cmd.getDomainId(); + Long projectId = cmd.getProjectId(); + + Ternary<Long, Boolean, Project.ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(domainId, cmd.isRecursive(), null); + accountMgr.buildACLSearchParameters(caller, id, accountName, projectId, permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + domainId = domainIdRecursiveListProject.first(); + Boolean isRecursive = domainIdRecursiveListProject.second(); + Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + Filter searchFilter = new Filter(FileShareVO.class, "created", false, startIndex, pageSize); + + SearchBuilder<FileShareVO> fileShareSearchBuilder = fileShareDao.createSearchBuilder(); + fileShareSearchBuilder.select(null, SearchCriteria.Func.DISTINCT, fileShareSearchBuilder.entity().getId()); // select distinct + accountMgr.buildACLSearchBuilder(fileShareSearchBuilder, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); + + fileShareSearchBuilder.and("id", fileShareSearchBuilder.entity().getId(), SearchCriteria.Op.EQ); + fileShareSearchBuilder.and("name", fileShareSearchBuilder.entity().getName(), SearchCriteria.Op.EQ); + fileShareSearchBuilder.and("dataCenterId", fileShareSearchBuilder.entity().getDataCenterId(), SearchCriteria.Op.EQ); + + if (keyword != null) { + fileShareSearchBuilder.and().op("keywordName", fileShareSearchBuilder.entity().getName(), SearchCriteria.Op.LIKE); Review Comment: .op will open a new parenthesis -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: commits-unsubscr...@cloudstack.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org