compute: Migrate 'host set' to SDK
This was the sole outstanding command to be migrated to SDK. We also clean up the old in-tree wrappers we have in the process and add missing error checks for the 'host list' and 'host show' commands. Change-Id: I5635469b63ab3370fb5118e4f8a1758002381aa5 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
0f07c97e84
commit
7252a7a781
@ -19,7 +19,7 @@ from osc_lib import exceptions
|
||||
from osc_lib.i18n import _
|
||||
|
||||
|
||||
# TODO(dtroyer): Mingrate this to osc-lib
|
||||
# TODO(dtroyer): Migrate this to osc-lib
|
||||
class InvalidValue(Exception):
|
||||
"""An argument value is not valid: wrong type, out of range, etc"""
|
||||
|
||||
@ -269,80 +269,6 @@ class APIv2(api.BaseAPI):
|
||||
|
||||
return self.list(url)["floating_ip_pools"]
|
||||
|
||||
# Hosts
|
||||
|
||||
def host_list(
|
||||
self,
|
||||
zone=None,
|
||||
):
|
||||
"""Lists hypervisor Hosts
|
||||
|
||||
https://docs.openstack.org/api-ref/compute/#list-hosts
|
||||
Valid for Compute 2.0 - 2.42
|
||||
|
||||
:param string zone:
|
||||
Availability zone
|
||||
:returns: A dict of the floating IP attributes
|
||||
"""
|
||||
|
||||
url = "/os-hosts"
|
||||
if zone:
|
||||
url = '/os-hosts?zone=%s' % zone
|
||||
|
||||
return self.list(url)["hosts"]
|
||||
|
||||
def host_set(
|
||||
self, host=None, status=None, maintenance_mode=None, **params
|
||||
):
|
||||
"""Modify host properties
|
||||
|
||||
https://docs.openstack.org/api-ref/compute/#update-host-status
|
||||
Valid for Compute 2.0 - 2.42
|
||||
|
||||
status
|
||||
maintenance_mode
|
||||
"""
|
||||
|
||||
url = "/os-hosts"
|
||||
|
||||
params = {}
|
||||
if status:
|
||||
params['status'] = status
|
||||
if maintenance_mode:
|
||||
params['maintenance_mode'] = maintenance_mode
|
||||
if params == {}:
|
||||
# Don't bother calling if nothing given
|
||||
return None
|
||||
else:
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/{url}/{host}",
|
||||
json=params,
|
||||
).json()
|
||||
|
||||
def host_show(
|
||||
self,
|
||||
host=None,
|
||||
):
|
||||
"""Show host
|
||||
|
||||
https://docs.openstack.org/api-ref/compute/#show-host-details
|
||||
Valid for Compute 2.0 - 2.42
|
||||
"""
|
||||
|
||||
url = "/os-hosts"
|
||||
|
||||
r_host = self.find(
|
||||
url,
|
||||
attr='host_name',
|
||||
value=host,
|
||||
)
|
||||
|
||||
data = []
|
||||
for h in r_host:
|
||||
data.append(h['resource'])
|
||||
return data
|
||||
|
||||
# Networks
|
||||
|
||||
def network_create(
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
"""Host action implementations"""
|
||||
|
||||
from openstack import exceptions as sdk_exceptions
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
|
||||
@ -35,34 +36,26 @@ class ListHost(command.Lister):
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
compute_client = self.app.client_manager.sdk_connection.compute
|
||||
columns = ("Host Name", "Service", "Zone")
|
||||
|
||||
self.log.warning(
|
||||
"API has been deprecated. "
|
||||
"Please consider using 'hypervisor list' instead."
|
||||
"API has been deprecated; "
|
||||
"consider using 'hypervisor list' instead."
|
||||
)
|
||||
|
||||
# doing this since openstacksdk has decided not to support this
|
||||
# deprecated command
|
||||
hosts = (
|
||||
compute_client.get('/os-hosts', microversion='2.1')
|
||||
.json()
|
||||
.get('hosts')
|
||||
)
|
||||
|
||||
response = compute_client.get('/os-hosts', microversion='2.1')
|
||||
sdk_exceptions.raise_from_response(response)
|
||||
hosts = response.json().get('hosts')
|
||||
if parsed_args.zone is not None:
|
||||
filtered_hosts = []
|
||||
for host in hosts:
|
||||
if host['zone'] == parsed_args.zone:
|
||||
filtered_hosts.append(host)
|
||||
|
||||
hosts = filtered_hosts
|
||||
hosts = [h for h in hosts if h['zone'] == parsed_args.zone]
|
||||
|
||||
columns = ("Host Name", "Service", "Zone")
|
||||
return columns, (utils.get_dict_properties(s, columns) for s in hosts)
|
||||
|
||||
|
||||
class SetHost(command.Command):
|
||||
_description = _("Set host properties")
|
||||
_description = _("DEPRECATED: Set host properties")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super().get_parser(prog_name)
|
||||
@ -90,20 +83,33 @@ class SetHost(command.Command):
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
kwargs = {}
|
||||
compute_client = self.app.client_manager.sdk_connection.compute
|
||||
|
||||
self.log.warning(
|
||||
"API has been deprecated; "
|
||||
"consider using 'compute service set' instead."
|
||||
)
|
||||
|
||||
data = {}
|
||||
if parsed_args.enable:
|
||||
kwargs['status'] = 'enable'
|
||||
data['status'] = 'enable'
|
||||
if parsed_args.disable:
|
||||
kwargs['status'] = 'disable'
|
||||
data['status'] = 'disable'
|
||||
if parsed_args.enable_maintenance:
|
||||
kwargs['maintenance_mode'] = 'enable'
|
||||
data['maintenance_mode'] = 'enable'
|
||||
if parsed_args.disable_maintenance:
|
||||
kwargs['maintenance_mode'] = 'disable'
|
||||
data['maintenance_mode'] = 'disable'
|
||||
|
||||
compute_client = self.app.client_manager.compute
|
||||
if not data:
|
||||
# don't bother calling if nothing given
|
||||
return
|
||||
|
||||
compute_client.api.host_set(parsed_args.host, **kwargs)
|
||||
# doing this since openstacksdk has decided not to support this
|
||||
# deprecated command
|
||||
response = compute_client.put(
|
||||
f'/os-hosts/{parsed_args.host}', json=data, microversion='2.1'
|
||||
)
|
||||
sdk_exceptions.raise_from_response(response)
|
||||
|
||||
|
||||
class ShowHost(command.Lister):
|
||||
@ -116,26 +122,25 @@ class ShowHost(command.Lister):
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
compute_client = self.app.client_manager.sdk_connection.compute
|
||||
columns = ("Host", "Project", "CPU", "Memory MB", "Disk GB")
|
||||
|
||||
self.log.warning(
|
||||
"API has been deprecated. "
|
||||
"Please consider using 'hypervisor show' instead."
|
||||
"API has been deprecated; "
|
||||
"consider using 'hypervisor show' instead."
|
||||
)
|
||||
|
||||
# doing this since openstacksdk has decided not to support this
|
||||
# deprecated command
|
||||
resources = (
|
||||
compute_client.get(
|
||||
'/os-hosts/' + parsed_args.host, microversion='2.1'
|
||||
)
|
||||
.json()
|
||||
.get('host')
|
||||
response = compute_client.get(
|
||||
f'/os-hosts/{parsed_args.host}', microversion='2.1'
|
||||
)
|
||||
sdk_exceptions.raise_from_response(response)
|
||||
resources = response.json().get('host')
|
||||
|
||||
data = []
|
||||
if resources is not None:
|
||||
for resource in resources:
|
||||
data.append(resource['resource'])
|
||||
|
||||
columns = ("Host", "Project", "CPU", "Memory MB", "Disk GB")
|
||||
|
||||
return columns, (utils.get_dict_properties(s, columns) for s in data)
|
||||
|
@ -227,114 +227,6 @@ class TestFloatingIPPool(TestComputeAPIv2):
|
||||
self.assertEqual(self.LIST_FLOATING_IP_POOL_RESP, ret)
|
||||
|
||||
|
||||
class TestHost(TestComputeAPIv2):
|
||||
FAKE_HOST_RESP_1 = {
|
||||
"zone": "internal",
|
||||
"host_name": "myhost",
|
||||
"service": "conductor",
|
||||
}
|
||||
|
||||
FAKE_HOST_RESP_2 = {
|
||||
"zone": "internal",
|
||||
"host_name": "myhost",
|
||||
"service": "scheduler",
|
||||
}
|
||||
|
||||
FAKE_HOST_RESP_3 = {
|
||||
"zone": "nova",
|
||||
"host_name": "myhost",
|
||||
"service": "compute",
|
||||
}
|
||||
|
||||
LIST_HOST_RESP = [
|
||||
FAKE_HOST_RESP_1,
|
||||
FAKE_HOST_RESP_2,
|
||||
FAKE_HOST_RESP_3,
|
||||
]
|
||||
|
||||
def test_host_list_no_options(self):
|
||||
self.requests_mock.register_uri(
|
||||
'GET',
|
||||
FAKE_URL + '/os-hosts',
|
||||
json={'hosts': self.LIST_HOST_RESP},
|
||||
status_code=200,
|
||||
)
|
||||
ret = self.api.host_list()
|
||||
self.assertEqual(self.LIST_HOST_RESP, ret)
|
||||
|
||||
def test_host_list_zone(self):
|
||||
self.requests_mock.register_uri(
|
||||
'GET',
|
||||
FAKE_URL + '/os-hosts?zone=nova',
|
||||
json={'hosts': [self.FAKE_HOST_RESP_3]},
|
||||
status_code=200,
|
||||
)
|
||||
self.requests_mock.register_uri(
|
||||
'GET',
|
||||
FAKE_URL + '/os-hosts',
|
||||
json={'hosts': [self.FAKE_HOST_RESP_3]},
|
||||
status_code=200,
|
||||
)
|
||||
ret = self.api.host_list(zone='nova')
|
||||
self.assertEqual([self.FAKE_HOST_RESP_3], ret)
|
||||
|
||||
def test_host_set_none(self):
|
||||
ret = self.api.host_set(host='myhost')
|
||||
self.assertIsNone(ret)
|
||||
|
||||
def test_host_set(self):
|
||||
self.requests_mock.register_uri(
|
||||
'PUT',
|
||||
FAKE_URL + '/os-hosts/myhost',
|
||||
json={},
|
||||
status_code=200,
|
||||
)
|
||||
ret = self.api.host_set(host='myhost', status='enabled')
|
||||
self.assertEqual({}, ret)
|
||||
|
||||
def test_host_show(self):
|
||||
FAKE_RESOURCE_1 = {
|
||||
"cpu": 2,
|
||||
"disk_gb": 1028,
|
||||
"host": "c1a7de0ac9d94e4baceae031d05caae3",
|
||||
"memory_mb": 8192,
|
||||
"project": "(total)",
|
||||
}
|
||||
FAKE_RESOURCE_2 = {
|
||||
"cpu": 0,
|
||||
"disk_gb": 0,
|
||||
"host": "c1a7de0ac9d94e4baceae031d05caae3",
|
||||
"memory_mb": 512,
|
||||
"project": "(used_now)",
|
||||
}
|
||||
FAKE_RESOURCE_3 = {
|
||||
"cpu": 0,
|
||||
"disk_gb": 0,
|
||||
"host": "c1a7de0ac9d94e4baceae031d05caae3",
|
||||
"memory_mb": 0,
|
||||
"project": "(used_max)",
|
||||
}
|
||||
FAKE_HOST_RESP = [
|
||||
{'resource': FAKE_RESOURCE_1},
|
||||
{'resource': FAKE_RESOURCE_2},
|
||||
{'resource': FAKE_RESOURCE_3},
|
||||
]
|
||||
FAKE_HOST_LIST = [
|
||||
FAKE_RESOURCE_1,
|
||||
FAKE_RESOURCE_2,
|
||||
FAKE_RESOURCE_3,
|
||||
]
|
||||
|
||||
self.requests_mock.register_uri(
|
||||
'GET',
|
||||
FAKE_URL + '/os-hosts/myhost',
|
||||
json={'host': FAKE_HOST_RESP},
|
||||
status_code=200,
|
||||
)
|
||||
ret = self.api.host_show(host='myhost')
|
||||
self.assertEqual(FAKE_HOST_LIST, ret)
|
||||
|
||||
|
||||
class TestNetwork(TestComputeAPIv2):
|
||||
FAKE_NETWORK_RESP = {
|
||||
'id': '1',
|
||||
|
@ -127,9 +127,6 @@ class FakeComputev2Client:
|
||||
self.keypairs = mock.Mock()
|
||||
self.keypairs.resource_class = fakes.FakeResource(None, {})
|
||||
|
||||
self.hosts = mock.Mock()
|
||||
self.hosts.resource_class = fakes.FakeResource(None, {})
|
||||
|
||||
self.server_groups = mock.Mock()
|
||||
self.server_groups.resource_class = fakes.FakeResource(None, {})
|
||||
|
||||
@ -1012,56 +1009,6 @@ def get_networks(networks=None, count=2):
|
||||
return mock.Mock(side_effect=networks)
|
||||
|
||||
|
||||
def create_one_host(attrs=None):
|
||||
"""Create a fake host.
|
||||
|
||||
:param dict attrs:
|
||||
A dictionary with all attributes
|
||||
:return:
|
||||
A FakeResource object, with uuid and other attributes
|
||||
"""
|
||||
attrs = attrs or {}
|
||||
|
||||
# Set default attributes.
|
||||
host_info = {
|
||||
"service_id": 1,
|
||||
"host": "host1",
|
||||
"uuid": 'host-id-' + uuid.uuid4().hex,
|
||||
"vcpus": 10,
|
||||
"memory_mb": 100,
|
||||
"local_gb": 100,
|
||||
"vcpus_used": 5,
|
||||
"memory_mb_used": 50,
|
||||
"local_gb_used": 10,
|
||||
"hypervisor_type": "xen",
|
||||
"hypervisor_version": 1,
|
||||
"hypervisor_hostname": "devstack1",
|
||||
"free_ram_mb": 50,
|
||||
"free_disk_gb": 50,
|
||||
"current_workload": 10,
|
||||
"running_vms": 1,
|
||||
"cpu_info": "",
|
||||
"disk_available_least": 1,
|
||||
"host_ip": "10.10.10.10",
|
||||
"supported_instances": "",
|
||||
"metrics": "",
|
||||
"pci_stats": "",
|
||||
"extra_resources": "",
|
||||
"stats": "",
|
||||
"numa_topology": "",
|
||||
"ram_allocation_ratio": 1.0,
|
||||
"cpu_allocation_ratio": 1.0,
|
||||
"zone": 'zone-' + uuid.uuid4().hex,
|
||||
"host_name": 'name-' + uuid.uuid4().hex,
|
||||
"service": 'service-' + uuid.uuid4().hex,
|
||||
"cpu": 4,
|
||||
"disk_gb": 100,
|
||||
'project': 'project-' + uuid.uuid4().hex,
|
||||
}
|
||||
host_info.update(attrs)
|
||||
return host_info
|
||||
|
||||
|
||||
def create_one_usage(attrs=None):
|
||||
"""Create a fake usage.
|
||||
|
||||
|
@ -11,9 +11,8 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
from openstackclient.compute.v2 import host
|
||||
from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
|
||||
@ -21,19 +20,51 @@ from openstackclient.tests.unit import fakes
|
||||
from openstackclient.tests.unit import utils as tests_utils
|
||||
|
||||
|
||||
@mock.patch('openstackclient.api.compute_v2.APIv2.host_list')
|
||||
def _generate_fake_host():
|
||||
return {
|
||||
'service_id': 1,
|
||||
'host': 'host1',
|
||||
'uuid': 'host-id-' + uuid.uuid4().hex,
|
||||
'vcpus': 10,
|
||||
'memory_mb': 100,
|
||||
'local_gb': 100,
|
||||
'vcpus_used': 5,
|
||||
'memory_mb_used': 50,
|
||||
'local_gb_used': 10,
|
||||
'hypervisor_type': 'xen',
|
||||
'hypervisor_version': 1,
|
||||
'hypervisor_hostname': 'devstack1',
|
||||
'free_ram_mb': 50,
|
||||
'free_disk_gb': 50,
|
||||
'current_workload': 10,
|
||||
'running_vms': 1,
|
||||
'cpu_info': '',
|
||||
'disk_available_least': 1,
|
||||
'host_ip': '10.10.10.10',
|
||||
'supported_instances': '',
|
||||
'metrics': '',
|
||||
'pci_stats': '',
|
||||
'extra_resources': '',
|
||||
'stats': '',
|
||||
'numa_topology': '',
|
||||
'ram_allocation_ratio': 1.0,
|
||||
'cpu_allocation_ratio': 1.0,
|
||||
'zone': 'zone-' + uuid.uuid4().hex,
|
||||
'host_name': 'name-' + uuid.uuid4().hex,
|
||||
'service': 'service-' + uuid.uuid4().hex,
|
||||
'cpu': 4,
|
||||
'disk_gb': 100,
|
||||
'project': 'project-' + uuid.uuid4().hex,
|
||||
}
|
||||
|
||||
|
||||
class TestHostList(compute_fakes.TestComputev2):
|
||||
_host = compute_fakes.create_one_host()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.compute_sdk_client.get.return_value = fakes.FakeResponse(
|
||||
data={'hosts': [self._host]}
|
||||
)
|
||||
|
||||
self._host = _generate_fake_host()
|
||||
self.columns = ('Host Name', 'Service', 'Zone')
|
||||
|
||||
self.data = [
|
||||
(
|
||||
self._host['host_name'],
|
||||
@ -42,10 +73,12 @@ class TestHostList(compute_fakes.TestComputev2):
|
||||
)
|
||||
]
|
||||
|
||||
self.compute_sdk_client.get.return_value = fakes.FakeResponse(
|
||||
data={'hosts': [self._host]}
|
||||
)
|
||||
self.cmd = host.ListHost(self.app, None)
|
||||
|
||||
def test_host_list_no_option(self, h_mock):
|
||||
h_mock.return_value = [self._host]
|
||||
def test_host_list_no_option(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
@ -59,8 +92,7 @@ class TestHostList(compute_fakes.TestComputev2):
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertEqual(self.data, list(data))
|
||||
|
||||
def test_host_list_with_option(self, h_mock):
|
||||
h_mock.return_value = [self._host]
|
||||
def test_host_list_with_option(self):
|
||||
arglist = [
|
||||
'--zone',
|
||||
self._host['zone'],
|
||||
@ -80,76 +112,60 @@ class TestHostList(compute_fakes.TestComputev2):
|
||||
self.assertEqual(self.data, list(data))
|
||||
|
||||
|
||||
@mock.patch('openstackclient.api.compute_v2.APIv2.host_set')
|
||||
class TestHostSet(compute_fakes.TestComputev2):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.host = compute_fakes.create_one_host()
|
||||
self._host = _generate_fake_host()
|
||||
self.compute_sdk_client.put.return_value = fakes.FakeResponse()
|
||||
|
||||
self.cmd = host.SetHost(self.app, None)
|
||||
|
||||
def test_host_set_no_option(self, h_mock):
|
||||
h_mock.return_value = self.host
|
||||
h_mock.update.return_value = None
|
||||
def test_host_set_no_option(self):
|
||||
arglist = [
|
||||
self.host['host'],
|
||||
self._host['host'],
|
||||
]
|
||||
verifylist = [
|
||||
('host', self.host['host']),
|
||||
('host', self._host['host']),
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
self.assertIsNone(result)
|
||||
self.compute_sdk_client.put.assert_not_called()
|
||||
|
||||
h_mock.assert_called_with(self.host['host'])
|
||||
|
||||
def test_host_set(self, h_mock):
|
||||
h_mock.return_value = self.host
|
||||
h_mock.update.return_value = None
|
||||
def test_host_set(self):
|
||||
arglist = [
|
||||
'--enable',
|
||||
'--disable-maintenance',
|
||||
self.host['host'],
|
||||
self._host['host'],
|
||||
]
|
||||
verifylist = [
|
||||
('enable', True),
|
||||
('enable_maintenance', False),
|
||||
('host', self.host['host']),
|
||||
('host', self._host['host']),
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
self.assertIsNone(result)
|
||||
|
||||
h_mock.assert_called_with(
|
||||
self.host['host'], status='enable', maintenance_mode='disable'
|
||||
self.compute_sdk_client.put.assert_called_with(
|
||||
f'/os-hosts/{self._host["host"]}',
|
||||
json={
|
||||
'maintenance_mode': 'disable',
|
||||
'status': 'enable',
|
||||
},
|
||||
microversion='2.1',
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('openstackclient.api.compute_v2.APIv2.host_show')
|
||||
class TestHostShow(compute_fakes.TestComputev2):
|
||||
_host = compute_fakes.create_one_host()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
output_data = {
|
||||
"resource": {
|
||||
"host": self._host['host'],
|
||||
"project": self._host['project'],
|
||||
"cpu": self._host['cpu'],
|
||||
"memory_mb": self._host['memory_mb'],
|
||||
"disk_gb": self._host['disk_gb'],
|
||||
}
|
||||
}
|
||||
|
||||
self.compute_sdk_client.get.return_value = fakes.FakeResponse(
|
||||
data={'host': [output_data]}
|
||||
)
|
||||
self._host = _generate_fake_host()
|
||||
|
||||
self.columns = (
|
||||
'Host',
|
||||
@ -158,7 +174,6 @@ class TestHostShow(compute_fakes.TestComputev2):
|
||||
'Memory MB',
|
||||
'Disk GB',
|
||||
)
|
||||
|
||||
self.data = [
|
||||
(
|
||||
self._host['host'],
|
||||
@ -169,10 +184,25 @@ class TestHostShow(compute_fakes.TestComputev2):
|
||||
)
|
||||
]
|
||||
|
||||
self.compute_sdk_client.get.return_value = fakes.FakeResponse(
|
||||
data={
|
||||
'host': [
|
||||
{
|
||||
'resource': {
|
||||
'host': self._host['host'],
|
||||
'project': self._host['project'],
|
||||
'cpu': self._host['cpu'],
|
||||
'memory_mb': self._host['memory_mb'],
|
||||
'disk_gb': self._host['disk_gb'],
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.cmd = host.ShowHost(self.app, None)
|
||||
|
||||
def test_host_show_no_option(self, h_mock):
|
||||
h_mock.host_show.return_value = [self._host]
|
||||
def test_host_show_no_option(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
@ -185,8 +215,7 @@ class TestHostShow(compute_fakes.TestComputev2):
|
||||
verifylist,
|
||||
)
|
||||
|
||||
def test_host_show_with_option(self, h_mock):
|
||||
h_mock.return_value = [self._host]
|
||||
def test_host_show_with_option(self):
|
||||
arglist = [
|
||||
self._host['host_name'],
|
||||
]
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
The ``host set`` command has been migrated to SDK.
|
Loading…
Reference in New Issue
Block a user