diff --git a/api/src/com/cloud/server/ManagementService.java b/api/src/com/cloud/server/ManagementService.java index 7f3141bc418..1f5a5be8cfc 100644 --- a/api/src/com/cloud/server/ManagementService.java +++ b/api/src/com/cloud/server/ManagementService.java @@ -401,7 +401,7 @@ public interface ManagementService { * @return Ternary, List, Map> List of all Hosts to which a VM * can be migrated, list of Hosts with enough capacity and hosts requiring storage motion for migration. */ - Ternary, Integer>, List, Map> listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize); + Ternary, Integer>, List, Map> listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize, String keyword); /** * List storage pools for live migrating of a volume. The API returns list of all pools in the cluster to which the diff --git a/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java b/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java index dbb94384523..ad9b6af3436 100644 --- a/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java +++ b/api/src/org/apache/cloudstack/api/command/admin/host/FindHostsForMigrationCmd.java @@ -76,7 +76,7 @@ public class FindHostsForMigrationCmd extends BaseListCmd { Map hostsRequiringStorageMotion; Ternary, Integer>, List, Map> hostsForMigration = - _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal()); + _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal(), this.getKeyword()); result = hostsForMigration.first(); List hostsWithCapacity = hostsForMigration.second(); hostsRequiringStorageMotion = hostsForMigration.third(); diff --git a/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java b/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java index 3391fdca1e3..9a5d3115b59 100644 --- a/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java +++ b/api/src/org/apache/cloudstack/api/command/admin/host/ListHostsCmd.java @@ -209,7 +209,7 @@ public class ListHostsCmd extends BaseListCmd { } else { Pair, Integer> result; Ternary, Integer>, List, Map> hostsForMigration = - _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal()); + _mgr.listHostsForMigrationOfVM(getVirtualMachineId(), this.getStartIndex(), this.getPageSizeVal(), null); result = hostsForMigration.first(); List hostsWithCapacity = hostsForMigration.second(); List hostResponses = new ArrayList(); diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index 29c99f2f225..574fa61721d 100644 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -1112,7 +1112,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } @Override - public Ternary, Integer>, List, Map> listHostsForMigrationOfVM(final Long vmId, final Long startIndex, final Long pageSize) { + public Ternary, Integer>, List, Map> + listHostsForMigrationOfVM(final Long vmId, + final Long startIndex, + final Long pageSize, final String keyword) { final Account caller = getCaller(); if (!_accountMgr.isRootAdmin(caller.getId())) { if (s_logger.isDebugEnabled()) { @@ -1200,9 +1203,8 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe List allHosts = null; final Map requiresStorageMotion = new HashMap(); DataCenterDeployment plan = null; - if (canMigrateWithStorage) { - allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, srcHost.getDataCenterId(), null, null, null, null, null, null, + allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, srcHost.getDataCenterId(), null, null, null, keyword, null, null, srcHost.getHypervisorType(), srcHost.getHypervisorVersion()); allHosts = allHostsPair.first(); allHosts.remove(srcHost); @@ -1239,7 +1241,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe if (s_logger.isDebugEnabled()) { s_logger.debug("Searching for all hosts in cluster " + cluster + " for migrating VM " + vm); } - allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, null, null, cluster, null, null, null, null, null, null); + allHostsPair = searchForServers(startIndex, pageSize, null, hostType, null, null, null, cluster, null, keyword, null, null, null, null); // Filter out the current host. allHosts = allHostsPair.first(); allHosts.remove(srcHost); diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 950c22a61b6..40d503c04f2 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -1425,6 +1425,27 @@ div.list-view td.state.transition span { background-position: 1px -432px; } +div.list-view td.state.suitable span { + background: url(../images/icons.png) no-repeat scroll 1px -224px; + color: #008000; + height: 18px; +} + +div.list-view td.state.suitable-storage-migration-required span { + width: 200px; +} + +div.list-view td.state.notsuitable span { + background: url(../images/icons.png) no-repeat scroll 1px -190px; + color: #B90606; + height: 19px; + width: 100px; +} + +div.list-view td.state.notsuitable-storage-migration-required span { + width: 220px !important; +} + .horizontal-overflow tbody td, .horizontal-overflow thead th { min-width: 40px; padding: 10px 10px 5px 0px; @@ -13232,3 +13253,21 @@ ul.ui-autocomplete.ui-menu { font-size: 13px; padding: 5px; } + +.multi-edit-add-list .ui-button.migrateok, +.multi-edit-add-list .ui-button.migratecancel { + top: -5px !important; +} + +.migrate-vm-available-host-list div.text-search { + right: 30px; +} + +.migrate-vm-available-host-list div.ui-widget-content { + display: block !important; +} + +.list-view-select table th.availableHostSuitability, +.list-view-select table td.availableHostSuitability { + max-width: 250px; +} \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index 9402153937a..7d0007adeaa 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1852,6 +1852,7 @@ + diff --git a/ui/l10n/en.js b/ui/l10n/en.js index f25e0b70486..bc79383dc19 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1466,6 +1466,7 @@ var dictionary = {"ICMP.code":"ICMP Code", "label.select.a.template":"Select a template", "label.select.a.zone":"Select a zone", "label.select.instance":"Select instance", +"label.select.host":"Select host", "label.select.instance.to.attach.volume.to":"Select instance to attach volume to", "label.select.iso.or.template":"Select ISO or template", "label.select.offering":"Select offering", @@ -1526,6 +1527,7 @@ var dictionary = {"ICMP.code":"ICMP Code", "label.start.vlan":"Start VLAN", "label.start.vxlan":"Start VXLAN", "label.state":"State", +"label.suitability": "Suitability", "label.static.nat":"Static NAT", "label.static.nat.enabled":"Static NAT Enabled", "label.static.nat.to":"Static NAT to", @@ -2114,6 +2116,7 @@ var dictionary = {"ICMP.code":"ICMP Code", "message.lock.account":"Please confirm that you want to lock this account. By locking the account, all users for this account will no longer be able to manage their cloud resources. Existing resources can still be accessed.", "message.migrate.instance.confirm":"Please confirm the host you wish to migrate the virtual instance to.", "message.migrate.instance.to.host":"Please confirm that you want to migrate instance to another host.", +"message.migrate.instance.select.host":"Please select a host for migration", "message.migrate.instance.to.ps":"Please confirm that you want to migrate instance to another primary storage.", "message.migrate.router.confirm":"Please confirm the host you wish to migrate the router to:", "message.migrate.systemvm.confirm":"Please confirm the host you wish to migrate the system VM to:", @@ -2123,7 +2126,8 @@ var dictionary = {"ICMP.code":"ICMP Code", "message.network.remote.access.vpn.configuration":"Remote Access VPN configuration has been generated, but it failed to apply. Please check connectivity of the network element, then re-try.", "message.new.user":"Specify the following to add a new user to the account", "message.no.affinity.groups":"You do not have any affinity groups. Please continue to the next step.", -"message.no.host.available":"No Hosts are available for Migration", +"message.no.host.available":"No hosts are available for migration", +"message.no.more.hosts.available":"No more hosts are available for migration", "message.no.network.support":"Your selected hypervisor, vSphere, does not have any additional network features. Please continue to step 5.", "message.no.network.support.configuration.not.true":"You do not have any zone that has security group enabled. Thus, no additional network features. Please continue to step 5.", "message.no.projects":"You do not have any projects.
Please create a new one from the projects section.", diff --git a/ui/l10n/ja_JP.js b/ui/l10n/ja_JP.js index 9dec16ffef7..797b351358e 100644 --- a/ui/l10n/ja_JP.js +++ b/ui/l10n/ja_JP.js @@ -1468,6 +1468,7 @@ var dictionary = { "label.select.a.zone": "ゾーンの選択", "label.select.instance": "インスタンスの選択", "label.select.instance.to.attach.volume.to": "ボリュームをアタッチするインスタンスを選択してください", + "label.select.host":"ホストの選択", "label.select.iso.or.template": "ISO またはテンプレートの選択", "label.select.offering": "オファリングの選択", "label.select.project": "プロジェクトの選択", @@ -1527,6 +1528,7 @@ var dictionary = { "label.start.vlan": "開始 VLAN", "label.start.vxlan": "開始 VXLAN", "label.state": "状態", + "label.suitability": "適合", "label.static.nat": "静的 NAT", "label.static.nat.enabled": "静的 NAT 有効", "label.static.nat.to": "静的 NAT の設定先:", @@ -2115,6 +2117,7 @@ var dictionary = { "message.lock.account": "このアカウントをロックしてもよろしいですか? このアカウントのすべてのユーザーがクラウド リソースを管理できなくなります。その後も既存のリソースにはアクセスできます。", "message.migrate.instance.confirm": "仮想インスタンスの移行先は次のホストでよろしいですか?", "message.migrate.instance.to.host": "別のホストにインスタンスを移行してもよろしいですか?", + "message.migrate.instance.select.host": "マイグレーション行うホストを選択。", "message.migrate.instance.to.ps": "別のプライマリ ストレージにインスタンスを移行してもよろしいですか?", "message.migrate.router.confirm": "ルーターの移行先は次のホストでよろしいですか?", "message.migrate.systemvm.confirm": "システム VM の移行先は次のホストでよろしいですか?", @@ -2125,6 +2128,7 @@ var dictionary = { "message.new.user": "アカウントに新しいユーザーを追加するために、次の情報を指定してください。", "message.no.affinity.groups": "アフィニティ グループがありません。次の手順に進んでください。", "message.no.host.available": "移行に使用できるホストはありません", + "message.no.more.hosts.available": "マイグレーション可能なホストがありません。", "message.no.network.support": "ハイパーバイザーとして vSphere を選択しましたが、このハイパーバイザーに追加のネットワーク機能はありません。手順 5. に進んでください。", "message.no.network.support.configuration.not.true": "セキュリティ グループが有効なゾーンが無いため、追加のネットワーク機能はありません。手順 5. に進んでください。", "message.no.projects": "プロジェクトがありません。
プロジェクト セクションから新しいプロジェクトを作成してください。", diff --git a/ui/l10n/zh_CN.js b/ui/l10n/zh_CN.js index accfc5b0dc9..2ba1c1f3197 100644 --- a/ui/l10n/zh_CN.js +++ b/ui/l10n/zh_CN.js @@ -1468,6 +1468,7 @@ var dictionary = { "label.select.a.zone": "选择一个资源域", "label.select.instance": "选择实例", "label.select.instance.to.attach.volume.to": "选择要将卷附加到的实例", + "label.select.host": "选择主机", "label.select.iso.or.template": "选择 ISO 或模板", "label.select.offering": "选择方案", "label.select.project": "选择项目", @@ -1527,6 +1528,7 @@ var dictionary = { "label.start.vlan": "起始 VLAN", "label.start.vxlan": "起始 VXLAN", "label.state": "状态", + "label.suitability": "适应性", "label.static.nat": "静态 NAT", "label.static.nat.enabled": "已启用静态 NAT", "label.static.nat.to": "静态 NAT 目标", @@ -2115,6 +2117,7 @@ var dictionary = { "message.lock.account": "请确认您确实要锁定此帐户。通过锁定此帐户,此帐户的所有用户将不再能够管理各自的云资源,但仍然可以访问现有资源。", "message.migrate.instance.confirm": "请确认要将虚拟实例迁移到的主机。", "message.migrate.instance.to.host": "请确认您确实要将实例迁移到其他主机。", + "message.migrate.instance.select.host": "选择用于迁移的主机", "message.migrate.instance.to.ps": "请确认您确实要将实例迁移到其他主存储。", "message.migrate.router.confirm": "请确认您要将路由器迁移到的主机:", "message.migrate.systemvm.confirm": "请确认您要将系统 VM 迁移到的主机:", @@ -2125,6 +2128,7 @@ var dictionary = { "message.new.user": "请指定以下信息以向帐户中添加一个新用户", "message.no.affinity.groups": "您没有任何关联性组。请继续执行下一步操作。", "message.no.host.available": "没有可用于迁移的主机", + "message.no.more.hosts.available": "没有可用于迁移的主机", "message.no.network.support": "您选择的虚拟机管理程序 vSphere 没有任何其他网络功能。请继续执行步骤 5。", "message.no.network.support.configuration.not.true": "您的所有资源域都未启用安全组,因此无其他网络功能。请继续执行步骤 5。", "message.no.projects": "您没有任何项目。
请从“项目”部分中创建一个新项目。", diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 012d11e8516..fa6dbb7957b 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -1479,113 +1479,130 @@ label: 'label.migrate.instance.to.host', compactLabel: 'label.migrate.to.host', messages: { - confirm: function(args) { - return 'message.migrate.instance.to.host'; - }, notification: function(args) { return 'label.migrate.instance.to.host'; } }, - createForm: { - title: 'label.migrate.instance.to.host', - desc: '', - fields: { - hostId: { - label: 'label.host', - validation: { - required: true - }, - select: function(args) { - $.ajax({ - url: createURL("findHostsForMigration&VirtualMachineId=" + args.context.instances[0].id), - dataType: "json", - async: true, - success: function(json) { - if (json.findhostsformigrationresponse.host != undefined) { - vmMigrationHostObjs = json.findhostsformigrationresponse.host; - var items = []; - $(vmMigrationHostObjs).each(function() { - if (this.requiresStorageMotion == true) { + action: { + custom: cloudStack.uiCustom.migrate({ + listView: { + listView: { + id: 'availableHosts', + fields: { + availableHostName: { + label: 'label.name' + }, + availableHostSuitability: { + label: 'label.suitability', + indicator: { + 'Suitable': 'suitable', + 'Suitable-Storage migration required': 'suitable suitable-storage-migration-required', + 'Not Suitable': 'notsuitable', + 'Not Suitable-Storage migration required': 'notsuitable notsuitable-storage-migration-required' + } + }, + cpuused: { + label: 'label.cpu.utilized' + }, + memoryused: { + label: 'label.memory.used' + } + }, + dataProvider: function(args) { + var data = { + page: args.page, + pagesize: pageSize + }; + if (args.filterBy.search.value) { + data.keyword = args.filterBy.search.value; + } + $.ajax({ + url: createURL("findHostsForMigration&VirtualMachineId=" + args.context.instances[0].id), + data: data, + dataType: "json", + async: true, + success: function(json) { + if (json.findhostsformigrationresponse.host != undefined) { + vmMigrationHostObjs = json.findhostsformigrationresponse.host; + var items = []; + $(vmMigrationHostObjs).each(function() { + var suitability = (this.suitableformigration ? "Suitable" : "Not Suitable"); + if (this.requiresStorageMotion == true) { + suitability += ("-Storage migration required"); + } items.push({ id: this.id, - description: (this.name + " (" + (this.suitableformigration ? "Suitable, " : "Not Suitable, ") + "Storage migration required)") - }); - - } else { - items.push({ - id: this.id, - description: (this.name + " (" + (this.suitableformigration ? "Suitable" : "Not Suitable") + ")") + availableHostName: this.name, + availableHostSuitability: suitability, + cpuused: this.cpuused, + memoryused: (parseFloat(this.memoryused)/(1024.0*1024.0*1024.0)).toFixed(2) + ' GB' }); + }); + args.response.success({ + data: items + }); + } else if(args.page == 1) { + args.response.success({ + data: null + }); + } else { + cloudStack.dialog.notice({ + message: _l('message.no.more.hosts.available') + }); + } + } + }); + } + } + }, + action: function(args) { + var selectedHostObj; + if (args.context.selectedHost != null && args.context.selectedHost.length > 0) { + selectedHostObj = args.context.selectedHost[0]; + if (selectedHostObj.requiresStorageMotion == true) { + $.ajax({ + url: createURL("migrateVirtualMachineWithVolume&hostid=" + selectedHostObj.id + "&virtualmachineid=" + args.context.instances[0].id), + dataType: "json", + async: true, + success: function(json) { + var jid = json.migratevirtualmachinewithvolumeresponse.jobid; + args.response.success({ + _custom: { + jobId: jid, + getUpdatedItem: function(json) { + return json.queryasyncjobresultresponse.jobresult.virtualmachine; + }, + getActionFilter: function() { + return vmActionfilter; + } } }); + } + }); + } else { + $.ajax({ + url: createURL("migrateVirtualMachine&hostid=" + selectedHostObj.id + "&virtualmachineid=" + args.context.instances[0].id), + dataType: "json", + async: true, + success: function(json) { + var jid = json.migratevirtualmachineresponse.jobid; args.response.success({ - data: items + _custom: { + jobId: jid, + getUpdatedItem: function(json) { + return json.queryasyncjobresultresponse.jobresult.virtualmachine; + }, + getActionFilter: function() { + return vmActionfilter; + } + } }); - } else { - cloudStack.dialog.notice({ - message: _l('message.no.host.available') - }); //Only a single host in the set up } - } - }); + }); + } } } - } - }, - action: function(args) { - var selectedHostObj; - if (vmMigrationHostObjs != null) { - for (var i = 0; i < vmMigrationHostObjs.length; i++) { - if (vmMigrationHostObjs[i].id == args.data.hostId) { - selectedHostObj = vmMigrationHostObjs[i]; - break; - } - } - } - if (selectedHostObj == null) - return; - - if (selectedHostObj.requiresStorageMotion == true) { - $.ajax({ - url: createURL("migrateVirtualMachineWithVolume&hostid=" + args.data.hostId + "&virtualmachineid=" + args.context.instances[0].id), - dataType: "json", - async: true, - success: function(json) { - var jid = json.migratevirtualmachinewithvolumeresponse.jobid; - args.response.success({ - _custom: { - jobId: jid, - getUpdatedItem: function(json) { - return json.queryasyncjobresultresponse.jobresult.virtualmachine; - }, - getActionFilter: function() { - return vmActionfilter; - } - } - }); - } - }); - } else { - $.ajax({ - url: createURL("migrateVirtualMachine&hostid=" + args.data.hostId + "&virtualmachineid=" + args.context.instances[0].id), - dataType: "json", - async: true, - success: function(json) { - var jid = json.migratevirtualmachineresponse.jobid; - args.response.success({ - _custom: { - jobId: jid, - getUpdatedItem: function(json) { - return json.queryasyncjobresultresponse.jobresult.virtualmachine; - }, - getActionFilter: function() { - return vmActionfilter; - } - } - }); - } - }); - } + }) }, notification: { poll: pollAsyncJobResult diff --git a/ui/scripts/ui-custom/migrate.js b/ui/scripts/ui-custom/migrate.js new file mode 100644 index 00000000000..fa940703dbc --- /dev/null +++ b/ui/scripts/ui-custom/migrate.js @@ -0,0 +1,127 @@ +// 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.migrate = function(args) { + var listView = args.listView; + var action = args.action; + + return function(args) { + var context = args.context; + + var hostList = function(args) { + var $listView; + + var hosts = $.extend(true, {}, args.listView, { + context: context, + uiCustom: true + }); + + hosts.listView.actions = { + select: { + label: _l('label.select.host'), + type: 'radio', + action: { + uiCustom: function(args) { + var $item = args.$item; + var $input = $item.find('td.actions input:visible'); + + if ($input.attr('type') == 'checkbox') { + if ($input.is(':checked')) + $item.addClass('multi-edit-selected'); + else + $item.removeClass('multi-edit-selected'); + } else { + $item.siblings().removeClass('multi-edit-selected'); + $item.addClass('multi-edit-selected'); + } + } + } + } + }; + + $listView = $('
').listView(hosts); + + // Change action label + $listView.find('th.actions').html(_l('label.select')); + + return $listView; + }; + + var $dataList = hostList({ + listView: listView + }).dialog({ + dialogClass: 'multi-edit-add-list panel migrate-vm-available-host-list', + width: 825, + draggable: false, + title: _l('label.migrate.instance.to.host'), + buttons: [{ + text: _l('label.ok'), + 'class': 'ok migrateok', + click: function() { + var complete = args.complete; + var selectedHostObj = $dataList.find('tr.multi-edit-selected').data('json-obj'); + if(selectedHostObj != undefined) { + $dataList.fadeOut(function() { + action({ + context: $.extend(true, {}, context, { + selectedHost: [ + selectedHostObj + ] + }), + response: { + success: function(args) { + complete({ + _custom: args._custom, + $item: $('
'), + }); + }, + error: function(args) { + cloudStack.dialog.notice({ + message: args + }); + } + } + }); + }); + + $('div.overlay').fadeOut(function() { + $('div.overlay').remove(); + }); + } + else { + cloudStack.dialog.notice({ + message: _l('message.migrate.instance.select.host') + }); + } + } + }, { + text: _l('label.cancel'), + 'class': 'cancel migratecancel', + click: function() { + $dataList.fadeOut(function() { + $dataList.remove(); + }); + $('div.overlay').fadeOut(function() { + $('div.overlay').remove(); + $(':ui-dialog').dialog('destroy'); + }); + } + }] + }).parent('.ui-dialog').overlay(); + }; + }; +}(cloudStack, jQuery));