Project

General

Profile

« Previous | Next » 

Revision 870d4598

Added by Ondřej Ezr over 5 years ago

Fixes #2113 - VMware filter datastores

View differences:

app/controllers/api/v2/compute_resources_controller.rb
before_action :find_resource, :only => [:show, :update, :destroy, :available_images, :associate,
:available_clusters, :available_flavors, :available_folders,
:available_networks, :available_resource_pools, :available_security_groups, :available_storage_domains,
:available_zones, :available_storage_pods, :refresh_cache]
:available_zones, :available_storage_pods, :storage_domain, :storage_pod, :refresh_cache]
api :GET, "/compute_resources/", N_("List all compute resources")
param_group :taxonomy_scope, ::Api::V2::BaseController
......
param :id, :identifier, :required => true
param :cluster_id, String
def available_networks
@available_networks = @compute_resource.available_networks(params[:cluster_id])
@available_networks = @compute_resource.available_networks(params[:cluster_id].presence)
@total = @available_networks&.size
render :available_networks, :layout => 'api/v2/layouts/index_layout'
end
......
render :available_resource_pools, :layout => 'api/v2/layouts/index_layout'
end
api :GET, "/compute_resources/:id/storage_domains/:storage_domain_id", N_("List attributes for a given storage domain")
param :id, :identifier, :required => true
param :storage_domain_id, String, :required => true
def storage_domain
@storage_domain = @compute_resource.storage_domain(params[:storage_domain_id])
end
api :GET, "/compute_resources/:id/available_storage_domains", N_("List storage domains for a compute resource")
api :GET, "/compute_resources/:id/available_storage_domains/:storage_domain", N_("List attributes for a given storage domain")
api :GET, "/compute_resources/:id/available_clusters/:cluster_id/available_storage_domains", N_("List storage domains for a compute resource")
param :id, :identifier, :required => true
param :cluster_id, String
param :storage_domain, String
def available_storage_domains
@available_storage_domains = @compute_resource.available_storage_domains(params[:storage_domain])
if params[:storage_domain]
Foreman::Deprecation.api_deprecation_warning("use /compute_resources/:id/storage_domain/:storage_domain_id endpoind instead")
@available_storage_domains = [@compute_resource.storage_domain(params[:storage_domain])]
else
@available_storage_domains = @compute_resource.available_storage_domains(params[:cluster_id].presence)
end
@total = @available_storage_domains&.size
render :available_storage_domains, :layout => 'api/v2/layouts/index_layout'
end
api :GET, "/compute_resources/:id/storage_pods/:storage_pod_id", N_("List attributes for a given storage pod")
param :id, :identifier, :required => true
param :storage_pod_id, String, :required => true
def storage_pod
@storage_pod = @compute_resource.storage_pod(params[:storage_pod_id])
end
api :GET, "/compute_resources/:id/available_storage_pods", N_("List storage pods for a compute resource")
api :GET, "/compute_resources/:id/available_storage_pods/:storage_pod", N_("List attributes for a given storage pod")
api :GET, "/compute_resources/:id/available_clusters/:cluster_id/available_storage_pods", N_("List storage pods for a compute resource")
param :id, :identifier, :required => true
param :cluster_id, String
param :storage_pod, String
def available_storage_pods
@available_storage_pods = @compute_resource.available_storage_pods(params[:storage_pod])
if params[:storage_pod]
Foreman::Deprecation.api_deprecation_warning("use /compute_resources/:id/storage_pod/:storage_pod_id endpoind instead")
@available_storage_pods = [@compute_resource.storage_pod(params[:storage_pod])]
else
@available_storage_pods = @compute_resource.available_storage_pods(params[:cluster_id].presence)
end
@total = @available_storage_pods&.size
render :available_storage_pods, :layout => 'api/v2/layouts/index_layout'
end
......
def action_permission
case params[:action]
when 'available_images', 'available_clusters', 'available_flavors', 'available_folders', 'available_networks', 'available_resource_pools', 'available_security_groups', 'available_storage_domains', 'available_zones', 'associate', 'available_storage_pods', 'refresh_cache'
when 'available_images', 'available_clusters', 'available_flavors', 'available_folders', 'available_networks', 'available_resource_pools', 'available_security_groups', 'available_storage_domains', 'storage_domain', 'available_zones', 'associate', 'available_storage_pods', 'storage_pod', 'refresh_cache'
:view
else
super
app/helpers/compute_resources_vms_helper.rb
options
end
def libvirt_networks(compute)
networks = compute.networks
def libvirt_networks(compute_resource)
networks = compute_resource.networks
select = []
select << [_('Physical (Bridge)'), :bridge]
select << [_('Virtual (NAT)'), :network] if networks.any?
select
end
def vsphere_networks(compute_resource)
networks = compute_resource.networks
def vsphere_networks(compute_resource, cluster_id = nil)
networks = compute_resource.networks(cluster_id: cluster_id)
networks.map do |net|
net_id = net.id
net_name = net.name
......
display_delete_if_authorized(hash_for_compute_resource_vm_path(:compute_resource_id => @compute_resource, :id => vm.identity).merge(:auth_object => @compute_resource, :authorizer => authorizer), :class => 'btn btn-danger')
end
def vsphere_scsi_controllers(compute)
def vsphere_scsi_controllers(compute_resource)
scsi_controllers = {}
compute.scsi_controller_types.each { |type| scsi_controllers[type[:key]] = type[:title] }
compute_resource.scsi_controller_types.each { |type| scsi_controllers[type[:key]] = type[:title] }
scsi_controllers
end
app/models/compute_resource.rb
false
end
def storage_domain(storage_domain)
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
def storage_pod(storage_pod)
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
def available_zones
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
......
[]
end
def available_networks
def available_networks(cluster_id = nil)
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
......
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
def available_storage_domains(storage_domain = nil)
def available_storage_domains(cluster_id = nil)
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
def available_storage_pods(storage_pod = nil)
def available_storage_pods(cluster_id = nil)
raise ::Foreman::Exception.new(N_("Not implemented for %s"), provider_friendly_name)
end
app/models/compute_resources/foreman/model/ovirt.rb
networks({:cluster_id => cluster_id})
end
def available_storage_domains(storage_domain = nil)
def available_storage_domains(cluster_id = nil)
storage_domains
end
app/models/compute_resources/foreman/model/vmware.rb
dc_clusters.map(&:full_path).sort
end
# Params:
# +name+ identifier of the datastore - its name unique in given vCenter
def datastore(name)
cache.cache(:"datastore-#{name}") do
dc.datastores.get(name)
end
end
# ==== Options
#
# * +:cluster_id+ - Limits the datastores in response to the ones available to defined cluster
def datastores(opts = {})
if opts[:storage_domain]
cache.cache(:"datastores-#{opts[:storage_domain]}") do
name_sort(dc.datastores.get(opts[:storage_domain]))
end
else
cache.cache(:datastores) do
name_sort(dc.datastores.all(:accessible => true))
cache.cache(cachekey_with_cluster(:datastores, opts[:cluster_id])) do
name_sort(dc.datastores(cluster: opts[:cluster_id]).all(:accessible => true))
end
end
def storage_pod(name)
cache.cache(:"storage_pod-#{name}") do
begin
dc.storage_pods.get(name)
rescue RbVmomi::VIM::InvalidArgument
{} # Return an empty storage pod hash if vsphere does not support the feature
end
end
end
##
# Lists storage_pods for datastore/cluster.
# TODO: fog-vsphere doesn't support cluster base filtering, so the cluser_id is useless for now.
# ==== Options
#
# * +:cluster_id+ - Limits the datastores in response to the ones available to defined cluster
def storage_pods(opts = {})
if opts[:storage_pod]
cache.cache(:"storage_pods-#{opts[:storage_pod]}") do
begin
dc.storage_pods.get(opts[:storage_pod])
rescue RbVmomi::VIM::InvalidArgument
{} # Return an empty storage pod hash if vsphere does not support the feature
end
end
else
cache.cache(:storage_pods) do
begin
name_sort(dc.storage_pods.all())
rescue RbVmomi::VIM::InvalidArgument
[] # Return an empty set of storage pods if vsphere does not support the feature
end
cache.cache(cachekey_with_cluster(:storage_pods, opts[:cluster_id])) do
begin
name_sort(dc.storage_pods.all(cluster: opts[:cluster_id]))
rescue RbVmomi::VIM::InvalidArgument
[] # Return an empty set of storage pods if vsphere does not support the feature
end
end
end
def available_storage_pods(storage_pod = nil)
storage_pods({:storage_pod => storage_pod})
def available_storage_pods(cluster_id = nil)
storage_pods(cluster_id: cluster_id)
end
def folders
......
end
def networks(opts = {})
cache.cache(:networks) do
name_sort(dc.networks.all(:accessible => true))
cache_key = opts[:cluster_id].nil? ? :networks : :"networks-#{opts[:cluster_id]}"
cache.cache(cache_key) do
name_sort(dc.networks(cluster: opts[:cluster_id]).all(:accessible => true))
end
end
......
end
def available_networks(cluster_id = nil)
networks
networks(cluster_id: cluster_id)
end
def available_storage_domains(storage_domain = nil)
datastores({:storage_domain => storage_domain})
def storage_domain(storage_domain)
datastore(storage_domain)
end
def available_storage_domains(cluster_id = nil)
datastores(cluster_id: cluster_id)
end
def available_resource_pools(opts = {})
......
Foreman::Logging.exception('Failed to parse user-data template', e)
raise Foreman::Exception.new('The user-data template must be valid YAML for VM customization to work.')
end
def cachekey_with_cluster(key, cluster_id = nil)
cluster_id.nil? ? key.to_sym : "#{key}-#{cluster_id}".to_sym
end
end
end
app/registries/foreman/access_permissions.rb
:"api/v2/compute_resources" => [:index, :show, :available_images, :available_clusters, :available_folders,
:available_flavors, :available_networks, :available_resource_pools,
:available_security_groups, :available_storage_domains, :available_zones,
:available_storage_pods, :refresh_cache]
:available_storage_pods, :storage_pod, :storage_domain, :refresh_cache]
}
map.permission :create_compute_resources, {:compute_resources => [:new, :create].push(*ajax_actions),
:"api/v2/compute_resources" => [:create]
app/views/api/v2/compute_resources/storage_domain.rabl
object @storage_domain
attribute :name, :id, :capacity, :freespace, :uncommitted
app/views/api/v2/compute_resources/storage_pod.rabl
object @storage_pod
attribute :name, :id, :capacity, :freespace
app/views/compute_resources_vms/form/vmware/_base.html.erb
end %>
<%= selectable_f f, :cluster, compute_resource.clusters, { :include_blank => _('Please select a cluster') },
:class => "col-md-2", :disabled => !new_vm,
:label => _('Cluster'), :onchange => 'tfm.computeResource.vmware.getResourcePools(this)',
:label => _('Cluster'), :onchange => 'tfm.computeResource.vmware.onClusterChange(this)',
:help_inline => :indicator,
:data => {:url => resource_pools_compute_resource_path(compute_resource)} %>
:data => {:poolsurl => resource_pools_compute_resource_path(compute_resource), :networksurl => available_networks_api_compute_resource_path(compute_resource)} %>
<%= vsphere_resource_pools(f, compute_resource, !new_vm) %>
<%= select_f f, :path, compute_resource.folders, :path, :to_label , {}, { :label => _("Folder"), :class => "col-md-2", :disabled => !new_vm } %>
<%= select_f f, :guest_id, compute_resource.guest_types, :first, :last, {}, { :label => _("Guest OS"), :class => "col-md-2", :disabled => !new_vm } %>
......
storagePodsUrl: available_storage_pods_api_compute_resource_path(compute_resource)
},
volumes: f.object.volumes.map { |volume| volume.attributes.merge(:size_gb => volume.size_gb).deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym }.reject { |k,v| k == :Size } },
controllers: f.object.scsi_controllers
controllers: f.object.scsi_controllers,
cluster: f.object.cluster.to_s
}.to_json) %>
app/views/compute_resources_vms/form/vmware/_network.html.erb
:class => "col-md-3 vmware_type",
:label => _('NIC type'), :label_size => "col-md-3", :disabled => !new_vm
%>
<% cluster_id = params.fetch(:host, {}).fetch(:compute_attributes, {}).fetch(:cluster, nil).presence %>
<%= select_f f, :network, vsphere_networks(compute_resource), :first, :last, { },
:class => "col-md-3 vmware_network",
:label => _('Network'), :label_size => "col-md-3", :disabled => !new_vm
config/routes/api/v2.rb
get :available_networks, :on => :member
get :available_security_groups, :on => :member
get :available_storage_domains, :on => :member
get 'storage_domains/(:storage_domain_id)', :to => 'compute_resources#storage_domain', :on => :member
get 'available_storage_domains/(:storage_domain)', :to => 'compute_resources#available_storage_domains', :on => :member
get :available_storage_pods, :on => :member
get 'storage_pods/(:storage_pod_id)', :to => 'compute_resources#storage_pod', :on => :member
get 'available_storage_pods/(:storage_pod)', :to => 'compute_resources#available_storage_pods', :on => :member
get 'available_clusters/(:cluster_id)/available_networks', :to => 'compute_resources#available_networks', :on => :member
get 'available_clusters/(:cluster_id)/available_resource_pools', :to => 'compute_resources#available_resource_pools', :on => :member
get 'available_clusters/(:cluster_id)/available_storage_domains', :to => 'compute_resources#available_storage_domains', :on => :member
get 'available_clusters/(:cluster_id)/available_storage_pods', :to => 'compute_resources#available_storage_pods', :on => :member
get :available_zones, :on => :member
put :associate, :on => :member
put :refresh_cache, :on => :member
test/controllers/api/v2/compute_resources_controller_test.rb
end
end
test "should get specific vmware storage domain" do
storage_domain = Object.new
storage_domain.stubs(:name).returns('test_vmware_cluster')
storage_domain.stubs(:id).returns('my11-test35-uuid99')
test "should get specific vmware storage domain - the deprecated way" do
storage_domain = OpenStruct.new(id: 'my11-test35-uuid99', name: 'test_vmware_datastore')
Foreman::Model::Vmware.any_instance.expects(:available_storage_domains).with('test_vmware_cluster').returns([storage_domain])
Foreman::Model::Vmware.any_instance.expects(:storage_domain).with('test_vmware_datastore').returns(storage_domain)
get :available_storage_domains, params: { :id => compute_resources(:vmware).to_param, :storage_domain => 'test_vmware_cluster' }
Foreman::Deprecation.expects(:api_deprecation_warning)
get :available_storage_domains, params: { :id => compute_resources(:vmware).to_param, :storage_domain => 'test_vmware_datastore' }
assert_response :success
available_storage_domains = ActiveSupport::JSON.decode(@response.body)
assert_equal storage_domain.id, available_storage_domains['results'].first.try(:[], 'id')
end
test "should get specific vmware storage pod" do
storage_pod = Object.new
storage_pod.stubs(:name).returns('test_vmware_pod')
storage_pod.stubs(:id).returns('group-p123456')
test "should get specific vmware storage domain" do
storage_domain = OpenStruct.new(id: 'my11-test35-uuid99', name: 'test_vmware_datastore')
Foreman::Model::Vmware.any_instance.expects(:storage_domain).with('test_vmware_datastore').returns(storage_domain)
get :storage_domain, params: { :id => compute_resources(:vmware).to_param, :storage_domain_id => 'test_vmware_datastore' }
assert_response :success
storage_domain_response = ActiveSupport::JSON.decode(@response.body)
assert_equal storage_domain.id, storage_domain_response.try(:[], 'id')
end
test "should get specific vmware storage pod - the deprecated way" do
storage_pod = OpenStruct.new(id: 'group-p123456', name: 'test_vmware_pod')
Foreman::Model::Vmware.any_instance.expects(:storage_pod).with('test_vmware_pod').returns(storage_pod)
Foreman::Model::Vmware.any_instance.expects(:available_storage_pods).with('test_vmware_pod').returns([storage_pod])
Foreman::Deprecation.expects(:api_deprecation_warning)
get :available_storage_pods, params: { :id => compute_resources(:vmware).to_param, :storage_pod => 'test_vmware_pod' }
assert_response :success
......
assert_equal storage_pod.id, available_storage_pods['results'].first.try(:[], 'id')
end
test "should get specific vmware storage pod" do
storage_pod = OpenStruct.new(id: 'group-p123456', name: 'test_vmware_pod')
Foreman::Model::Vmware.any_instance.expects(:storage_pod).with('test_vmware_pod').returns(storage_pod)
get :storage_pod, params: { :id => compute_resources(:vmware).to_param, :storage_pod_id => 'test_vmware_pod' }
assert_response :success
storage_pod_response = ActiveSupport::JSON.decode(@response.body)
assert_equal storage_pod.id, storage_pod_response.try(:[], 'id')
end
test "should associate hosts that match" do
host_cr = FactoryBot.create(:host, :on_compute_resource)
host_bm = FactoryBot.create(:host)
webpack/assets/javascripts/compute_resource/ovirt.js
}
export function clusterSelected(item) {
const cluster = $(item).val();
const url = $(item).attr('data-url');
const url = $(item).data('url');
showSpinner();
$.ajax({
webpack/assets/javascripts/compute_resource/vmware.js
import $ from 'jquery';
import store from '../react_app/redux';
import { showSpinner, hideSpinner } from '../foreman_tools';
import { changeCluster } from '../react_app/redux/actions/hosts/storage/vmware';
export function getResourcePools(item) {
export function onClusterChange(item) {
const clusterId = $(item).val();
const resPoolsUrl = $(item).data('poolsurl');
const networksUrl = $(item).data('networksurl');
store.dispatch(changeCluster(clusterId));
fetchResourcePools(resPoolsUrl, clusterId);
fetchNetworks(networksUrl, clusterId);
}
function fetchResourcePools(url, clusterId) {
// eslint-disable-next-line camelcase
const data = { cluster_id: $(item).val() };
const url = $(item).data('url');
const data = { cluster_id: clusterId };
showSpinner();
const selectbox = $('select[id$="resource_pool"]');
selectbox.select2('destroy').empty();
$.ajax({
type: 'get',
url,
......
hideSpinner();
},
success(request) {
selectbox.select2('destroy').empty();
request.forEach(({ name }) => {
$('<option>')
.text(name)
......
},
});
}
function fetchNetworks(url, clusterId) {
const $networkOptions = $('select[id$=_network]');
showSpinner();
$.ajax({
type: 'get',
url,
data: { cluster_id: clusterId },
success(response) {
$networkOptions.empty();
$.each(response.results, (idx, value) => {
$networkOptions.append(new Option(value.name, value.id, false, false));
});
window.update_interface_table();
},
complete() {
hideSpinner();
},
});
}
webpack/assets/javascripts/react_app/components/hosts/storage/vmware/StorageContainer.fixtures.js
},
],
controllers: [{ type: 'VirtualLsiLogicController', key: 1000 }],
cluster: 'Foreman_Cluster',
};
export const hiddenFieldValue = {
......
},
],
controllers: [{ type: 'VirtualLsiLogicController', key: 1000 }],
cluster: 'Foreman_Cluster',
};
export const state2 = {
......
unitNumber: 0,
},
],
cluster: 'Foreman_Cluster',
};
export const clone = {
......
key: 1001,
},
],
cluster: 'Foreman_Cluster',
};
export const emptyState = {
......
},
volumes: [],
controllers: [],
cluster: null,
};
webpack/assets/javascripts/react_app/components/hosts/storage/vmware/index.js
import { pick } from 'lodash';
import React from 'react';
import { Button } from 'react-bootstrap';
import { Alert } from 'patternfly-react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
......
import { MaxDisksPerController } from './StorageContainer.consts';
import { translate as __ } from '../../../../../react_app/common/I18n';
import { noop } from '../../../../common/helpers';
import AlertBody from '../../../common/Alert/AlertBody';
import './StorageContainer.scss';
import { STATUS } from '../../../../constants';
......
class StorageContainer extends React.Component {
componentDidMount() {
const {
data: { config, controllers, volumes },
data: { config, controllers, volumes, cluster },
initController,
fetchDatastores,
fetchStoragePods,
} = this.props;
initController(config, controllers, volumes);
fetchDatastores(config.datastoresUrl);
fetchStoragePods(config.storagePodsUrl);
initController(config, cluster, controllers, volumes);
}
getDatastoresStatus() {
......
}
render() {
const { addController, controllers, volumes, config } = this.props;
const { addController, controllers, volumes, cluster, config } = this.props;
const paramsScope = config && config.paramsScope;
const enableAddControllerBtn =
config && config.addControllerEnabled && !config.vmExists;
if (!cluster) {
return (
<Alert type="info">
<AlertBody message={__('Please select a cluster')} />
</Alert>
);
}
return (
<div className="row vmware-storage-container">
<div className="storage-header">
......
config: PropTypes.object.isRequired,
controllers: PropTypes.array.isRequired,
volumes: PropTypes.array.isRequired,
cluster: PropTypes.string,
}).isRequired,
controllers: PropTypes.array.isRequired,
config: PropTypes.object,
volumes: PropTypes.array,
cluster: PropTypes.string,
datastoresLoading: PropTypes.bool,
datastores: PropTypes.arrayOf(
PropTypes.shape({
......
updateDisk: PropTypes.func,
removeController: PropTypes.func,
initController: PropTypes.func,
fetchDatastores: PropTypes.func,
fetchStoragePods: PropTypes.func,
};
StorageContainer.defaultProps = {
config: {},
cluster: '',
volumes: [],
datastoresLoading: false,
storagePodsLoading: false,
......
updateDisk: noop,
removeController: noop,
initController: noop,
fetchDatastores: noop,
fetchStoragePods: noop,
};
const mapDispatchToProps = state => {
const {
controllers,
config,
volumes,
datastores,
datastoresLoading,
datastoresError,
storagePods,
storagePodsLoading,
storagePodsError,
} = state.hosts.storage.vmware;
return {
controllers,
volumes,
config,
datastores,
datastoresLoading,
datastoresError,
storagePods,
storagePodsLoading,
storagePodsError,
};
};
const mapStateToProps = state =>
pick(state.hosts.storage.vmware, [
'controllers',
'config',
'cluster',
'volumes',
'datastores',
'datastoresLoading',
'datastoresError',
'storagePods',
'storagePodsLoading',
'storagePodsError',
]);
export default connect(
mapDispatchToProps,
mapStateToProps,
VmWareActions
)(StorageContainer);
webpack/assets/javascripts/react_app/redux/actions/bookmarks/bookmarks.test.js
const expectedURL = '/api/bookmarks?search=controller%3Dhosts&per_page=100';
store.dispatch(actions.getBookmarks(url, controller));
expect(spy).toBeCalledWith(expectedURL);
expect(spy).toBeCalledWith(expectedURL, {}, {});
});
it('should open modal with current search query in action payload', () => {
const query = 'some search query';
webpack/assets/javascripts/react_app/redux/actions/common/index.js
successAction,
failedAction,
url,
item,
item = {},
}) => {
dispatch({ type: requestAction, payload: item });
return API.get(url)
return API.get(url, item.headers || {}, item.params || {})
.then(({ data }) =>
dispatch({ type: successAction, payload: { ...item, ...data } })
)
webpack/assets/javascripts/react_app/redux/actions/hosts/storage/vmware.fixtures.js
import {
VMWARE_CLUSTER_CHANGE,
STORAGE_VMWARE_INIT,
STORAGE_VMWARE_DATASTORES_REQUEST,
STORAGE_VMWARE_DATASTORES_SUCCESS,
STORAGE_VMWARE_DATASTORES_FAILURE,
STORAGE_VMWARE_STORAGEPODS_REQUEST,
STORAGE_VMWARE_STORAGEPODS_SUCCESS,
STORAGE_VMWARE_STORAGEPODS_FAILURE,
} from '../../../consts';
import {
defaultControllerAttributes,
getDefaultDiskAttributes,
} from './vmware.consts';
export const datastoresUrl = 'test.com/datastores';
export const storagePodsUrl = 'test.com/storage_pods';
const controllerTypes = {
VirtualBusLogicController: 'Bus Logic Parallel',
VirtualLsiLogicController: 'LSI Logic Parallel',
VirtualLsiLogicSASController: 'LSI Logic SAS',
ParaVirtualSCSIController: 'VMware Paravirtual',
};
const diskModeTypes = {
persistent: 'Persistent',
independent_persistent: 'Independent - Persistent',
independent_nonpersistent: 'Independent - Nonpersistent',
};
export const basicConfig = {
vmExists: false,
controllerTypes,
diskModeTypes,
paramsScope: 'host[storageParams]',
datastoresUrl,
storagePodsUrl,
};
export const initAction = {
type: STORAGE_VMWARE_INIT,
payload: {
config: basicConfig,
controllers: defaultControllerAttributes,
volumes: getDefaultDiskAttributes,
cluster: 'cluster',
},
};
export const changeClusterAction = {
type: VMWARE_CLUSTER_CHANGE,
payload: {
cluster: 'newCluster',
},
};
export const state1 = {
hosts: {
storage: {
vmware: {
config: basicConfig,
cluster: 'cluster',
},
},
},
};
export const fetchDatastoreParams = {
requestAction: STORAGE_VMWARE_DATASTORES_REQUEST,
successAction: STORAGE_VMWARE_DATASTORES_SUCCESS,
failedAction: STORAGE_VMWARE_DATASTORES_FAILURE,
url: datastoresUrl,
item: { params: { cluster_id: 'cluster' } },
};
export const fetchStoragePodsParams = {
requestAction: STORAGE_VMWARE_STORAGEPODS_REQUEST,
successAction: STORAGE_VMWARE_STORAGEPODS_SUCCESS,
failedAction: STORAGE_VMWARE_STORAGEPODS_FAILURE,
url: storagePodsUrl,
item: { params: { cluster_id: 'cluster' } },
};
webpack/assets/javascripts/react_app/redux/actions/hosts/storage/vmware.js
import {
VMWARE_CLUSTER_CHANGE,
STORAGE_VMWARE_ADD_CONTROLLER,
STORAGE_VMWARE_ADD_DISK,
STORAGE_VMWARE_REMOVE_CONTROLLER,
......
},
});
export const initController = (config, controllers, volumes) => dispatch => {
export const initController = (
config,
cluster,
controllers,
volumes
) => dispatch => {
dispatch({
type: STORAGE_VMWARE_INIT,
payload: {
config,
controllers: controllers || defaultControllerAttributes,
volumes: volumes || getDefaultDiskAttributes,
cluster,
},
});
dispatch(fetchDatastores(config.datastoresUrl, cluster));
dispatch(fetchStoragePods(config.storagePodsUrl, cluster));
};
export const fetchDatastores = url => dispatch => {
ajaxRequestAction({
dispatch,
export const changeCluster = newCluster => (dispatch, getState) => {
const { config } = getState().hosts.storage.vmware;
dispatch({
type: VMWARE_CLUSTER_CHANGE,
payload: {
cluster: newCluster,
},
});
dispatch(fetchDatastores(config.datastoresUrl, newCluster));
dispatch(fetchStoragePods(config.storagePodsUrl, newCluster));
};
const fetchStorages = (url, cluster = null, actions = {}) => dispatch => {
if (cluster) {
ajaxRequestAction({
dispatch,
...actions,
url,
item: { params: { cluster_id: cluster } },
});
}
};
export const fetchDatastores = (url, cluster = null) =>
fetchStorages(url, cluster, {
requestAction: STORAGE_VMWARE_DATASTORES_REQUEST,
successAction: STORAGE_VMWARE_DATASTORES_SUCCESS,
failedAction: STORAGE_VMWARE_DATASTORES_FAILURE,
url,
});
};
export const fetchStoragePods = url => dispatch => {
ajaxRequestAction({
dispatch,
export const fetchStoragePods = (url, cluster = null) =>
fetchStorages(url, cluster, {
requestAction: STORAGE_VMWARE_STORAGEPODS_REQUEST,
successAction: STORAGE_VMWARE_STORAGEPODS_SUCCESS,
failedAction: STORAGE_VMWARE_STORAGEPODS_FAILURE,
url,
});
};
export const addController = data => ({
type: STORAGE_VMWARE_ADD_CONTROLLER,
webpack/assets/javascripts/react_app/redux/actions/hosts/storage/vmware.test.js
import { ajaxRequestAction } from '../../common';
import {
datastoresUrl,
storagePodsUrl,
basicConfig,
initAction,
changeClusterAction,
state1,
fetchDatastoreParams,
fetchStoragePodsParams,
} from './vmware.fixtures';
import * as actions from './vmware';
jest.mock('../../common');
afterEach(() => {
ajaxRequestAction.mockReset();
});
describe('vmware storage hosts actions', () => {
describe('initController', () => {
it('initializes the container', () => {
const dispatch = jest.fn();
const dispatcher = actions.initController(
basicConfig,
'cluster',
null,
null
);
dispatcher(dispatch);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledWith(initAction);
});
});
describe('changeCluster', () => {
it('changes the cluster and refetches the storages', () => {
const dispatch = jest.fn();
const dispatcher = actions.changeCluster('newCluster');
dispatcher(dispatch, () => state1);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenCalledWith(changeClusterAction);
});
});
describe.each([
['fetchDatastores', datastoresUrl, fetchDatastoreParams],
['fetchStoragePods', storagePodsUrl, fetchStoragePodsParams],
])('%s', (actionName, url, fetchParams) => {
it('doesnt make the ajax request when cluster is not set', () => {
const dispatch = jest.fn();
const dispatcher = actions[actionName](url, null);
dispatcher(dispatch);
expect(ajaxRequestAction).not.toBeCalled();
});
it('makes the ajax request to the right url', () => {
const dispatch = jest.fn();
const dispatcher = actions[actionName](url, 'cluster');
dispatcher(dispatch);
expect(ajaxRequestAction).toBeCalledWith({
dispatch,
...fetchParams,
});
});
});
});
webpack/assets/javascripts/react_app/redux/consts.js
export const TOASTS_ADD = 'TOASTS_ADD';
export const TOASTS_CLEAR = 'TOASTS_CLEAR';
export const TOASTS_DELETE = 'TOASTS_DELETE';
export const VMWARE_CLUSTER_CHANGE = 'VMWARE_CLUSTER_CHANGE';
export const STORAGE_VMWARE_INIT = 'STORAGE_VMWARE_INIT';
export const STORAGE_VMWARE_ADD_CONTROLLER = 'STORAGE_VMWARE_ADD_CONTROLLER';
export const STORAGE_VMWARE_ADD_DISK = 'STORAGE_VMWARE_ADD_DISK';
webpack/assets/javascripts/react_app/redux/reducers/hosts/storage/__snapshots__/vmware.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vmware storage reducer STORAGE_VMWARE_ADD_CONTROLLER adds another controller 1`] = `
Object {
"controllers": Array [
Object {
"key": 1000,
"type": "ParaVirtualSCSIController",
},
Object {
"key": 1001,
"type": "ParaVirtualSCSIController",
},
],
"volumes": Array [
Object {
"controllerKey": 1000,
"datastore": "",
"eagerZero": false,
"key": "5124c2d1-339b-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
Object {
"controllerKey": 1001,
"datastore": "",
"eagerZero": false,
"key": "1547e1c0-309a-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
],
}
`;
exports[`vmware storage reducer STORAGE_VMWARE_ADD_CONTROLLER adds another controller to fill after removed one 1`] = `
Object {
"controllers": Array [
Object {
"key": 1001,
"type": "ParaVirtualSCSIController",
},
Object {
"key": 1000,
"type": "ParaVirtualSCSIController",
},
],
"volumes": Array [
Object {
"controllerKey": 1001,
"datastore": "",
"eagerZero": false,
"key": "5124c2d1-339b-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
Object {
"controllerKey": 1000,
"datastore": "",
"eagerZero": false,
"key": "1547e1c0-309a-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
],
}
`;
exports[`vmware storage reducer STORAGE_VMWARE_ADD_CONTROLLER adds controller to initialState 1`] = `
Object {
"controllers": Array [
Object {
"key": 1000,
"type": "ParaVirtualSCSIController",
},
],
"volumes": Array [
Object {
"controllerKey": 1000,
"datastore": "",
"eagerZero": false,
"key": "1547e1c0-309a-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
],
}
`;
exports[`vmware storage reducer STORAGE_VMWARE_ADD_DISK adds volume 1`] = `
Object {
"controllers": Array [
Object {
"key": 1000,
"type": "ParaVirtualSCSIController",
},
],
"volumes": Array [
Object {
"controllerKey": 1000,
"datastore": "",
"eagerZero": false,
"key": "5124c2d1-339b-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
Object {
"controllerKey": 1000,
"datastore": "",
"eagerZero": false,
"key": "1547e1c0-309a-11e9-98f5-5f761412a4c2",
"mode": "persistent",
"name": "Hard disk",
"sizeGb": 10,
"storagePod": "",
"thin": false,
},
],
}
`;
webpack/assets/javascripts/react_app/redux/reducers/hosts/storage/vmware.fixtures.js
import Immutable from 'seamless-immutable';
export const initialState = Immutable({
controllers: [],
volumes: [],
});
export const controllerAttributes = {
type: 'ParaVirtualSCSIController',
};
export const diskAttributes = {
datastore: '',
eagerZero: false,
mode: 'persistent',
name: 'Hard disk',
sizeGb: 10,
storagePod: '',
thin: false,
};
export const diskKey = '5124c2d1-339b-11e9-98f5-5f761412a4c2';
const _generateController = key =>
Immutable({
controllers: [
{
key,
type: 'ParaVirtualSCSIController',
},
],
volumes: [
{
controllerKey: key,
datastore: '',
eagerZero: false,
key: diskKey,
mode: 'persistent',
name: 'Hard disk',
sizeGb: 10,
storagePod: '',
thin: false,
},
],
});
export const stateWithController = _generateController(1000);
export const stateWithRemovedController = _generateController(1001);
webpack/assets/javascripts/react_app/redux/reducers/hosts/storage/vmware.js
import uuidV1 from 'uuid/v1';
import {
VMWARE_CLUSTER_CHANGE,
STORAGE_VMWARE_ADD_CONTROLLER,
STORAGE_VMWARE_ADD_DISK,
STORAGE_VMWARE_REMOVE_DISK,
......
const initialState = Immutable({
controllers: [],
volumes: [],
});
const availableControllerKeys = [1000, 1001, 1002, 1003, 1004];
......
export default (state = initialState, { type, payload }) => {
switch (type) {
case VMWARE_CLUSTER_CHANGE:
return state.set('cluster', payload.cluster);
case STORAGE_VMWARE_ADD_CONTROLLER:
const availableKey = getAvailableKey(state.controllers);
......
controllers: payload.controllers,
paramsScope: payload.config.paramsScope,
datastores: [],
datastoresLoading: true,
datastoresLoading: false,
datastoresError: undefined,
storagePods: [],
storagePodsLoading: true,
storagePodsLoading: false,
storagePodsError: undefined,
volumes: payload.volumes.map(volume => ({ ...volume, key: uuidV1() })),
cluster: payload.cluster,
};
return initialState
.set('config', payload.config)
webpack/assets/javascripts/react_app/redux/reducers/hosts/storage/vmware.test.js
import uuidV1 from 'uuid/v1';
import * as types from '../../../consts';
import {
diskKey,
initialState,
controllerAttributes,
diskAttributes,
stateWithController,
stateWithRemovedController,
} from './vmware.fixtures';
import reducer from './vmware';
jest.mock('uuid/v1');
uuidV1.mockImplementation(() => '1547e1c0-309a-11e9-98f5-5f761412a4c2');
describe('vmware storage reducer', () => {
it('returns the initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
it('handles cluster change', () => {
expect(
reducer(initialState, {
type: types.VMWARE_CLUSTER_CHANGE,
payload: { cluster: 'testCluster' },
})
).toEqual({ ...initialState, cluster: 'testCluster' });
});
describe('STORAGE_VMWARE_ADD_CONTROLLER', () => {
it('adds controller to initialState', () => {
expect(
reducer(initialState, {
type: types.STORAGE_VMWARE_ADD_CONTROLLER,
payload: {
controller: controllerAttributes,
volume: diskAttributes,
},
})
).toMatchSnapshot();
});
it('adds another controller', () => {
expect(
reducer(stateWithController, {
type: types.STORAGE_VMWARE_ADD_CONTROLLER,
payload: {
controller: controllerAttributes,
volume: diskAttributes,
},
})
).toMatchSnapshot();
});
it('adds another controller to fill after removed one', () => {
expect(
reducer(stateWithRemovedController, {
type: types.STORAGE_VMWARE_ADD_CONTROLLER,
payload: {
controller: controllerAttributes,
volume: diskAttributes,
},
})
).toMatchSnapshot();
});
});
describe('STORAGE_VMWARE_ADD_DISK', () => {
it('adds volume', () => {
expect(
reducer(stateWithController, {
type: types.STORAGE_VMWARE_ADD_DISK,
payload: {
controllerKey: 1000,
data: diskAttributes,
},
})
).toMatchSnapshot();
});
});
describe('STORAGE_VMWARE_UPDATE_DISK', () => {
it('update volume', () => {
const result = reducer(stateWithController, {
type: types.STORAGE_VMWARE_UPDATE_DISK,
payload: {
key: diskKey,
newValues: { sizeGb: 15 },
},
});
expect(result.volumes).toEqual([
{ ...stateWithController.volumes[0], sizeGb: 15 },
]);
});
});
});

Also available in: Unified diff