parameters:
- nodes: 3
+ nodes: 4
pool: ceph-dashboard
network: ceph-dashboard
domain: cephlab.com
@raise_if_no_orchestrator([OrchFeature.HOST_LABEL_ADD,
OrchFeature.HOST_LABEL_REMOVE,
OrchFeature.HOST_MAINTENANCE_ENTER,
- OrchFeature.HOST_MAINTENANCE_EXIT])
+ OrchFeature.HOST_MAINTENANCE_EXIT,
+ OrchFeature.HOST_DRAIN])
@handle_orchestrator_error('host')
@EndpointDoc('',
parameters={
'update_labels': (bool, 'Update Labels'),
'labels': ([str], 'Host Labels'),
'maintenance': (bool, 'Enter/Exit Maintenance'),
- 'force': (bool, 'Force Enter Maintenance')
+ 'force': (bool, 'Force Enter Maintenance'),
+ 'drain': (bool, 'Drain Host')
},
responses={200: None, 204: None})
@RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
def set(self, hostname: str, update_labels: bool = False,
labels: List[str] = None, maintenance: bool = False,
- force: bool = False):
+ force: bool = False, drain: bool = False):
"""
Update the specified host.
Note, this is only supported when Ceph Orchestrator is enabled.
:param labels: List of labels.
:param maintenance: Enter/Exit maintenance mode.
:param force: Force enter maintenance mode.
+ :param drain: Drain host
"""
orch = OrchClient.instance()
host = get_host(hostname)
if status == 'maintenance':
orch.hosts.exit_maintenance(hostname)
+ if drain:
+ orch.hosts.drain(hostname)
+
if update_labels:
# only allow List[str] type for labels
if not isinstance(labels, list):
});
}
- delete(hostname: string) {
+ remove(hostname: string) {
super.delete(hostname, this.columnIndex.hostname, 'hosts');
}
});
}
}
+
+ @PageHelper.restrictTo(pages.index.url)
+ drain(hostname: string) {
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ this.clickActionButton('start-drain');
+ this.checkLabelExists(hostname, ['_no_schedule'], true);
+
+ this.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ cy.wait(10000);
+ this.expectTableCount('total', 0);
+ });
+ }
}
hosts.add(hostname, true);
});
- it('should drain and delete a host and then add it back', function () {
+ it('should drain and remove a host and then add it back', function () {
const hostname = Cypress._.last(this.hosts)['name'];
// should drain the host first before deleting
- hosts.editLabels(hostname, ['_no_schedule'], true);
- hosts.clickTab('cd-host-details', hostname, 'Daemons');
- cy.get('cd-host-details').within(() => {
- // draining will take some time to complete.
- // since we don't know how many daemons will be
- // running in this host in future putting the wait
- // to 15s
- cy.wait(15000);
- hosts.getTableCount('total').should('be.eq', 0);
- });
- hosts.delete(hostname);
+ hosts.drain(hostname);
+ hosts.remove(hostname);
// add it back
hosts.navigateTo('add');
'ceph-node-00.cephlab.com',
'ceph-node-01.cephlab.com',
'ceph-node-02.cephlab.com',
- 'ceph-node-[01-02].cephlab.com'
+ 'ceph-node-[01-03].cephlab.com'
];
const addHost = (hostname: string, exist?: boolean, pattern?: boolean, labels: string[] = []) => {
cy.get('.btn.btn-accent').first().click({ force: true });
addHost(hostnames[1], false);
addHost(hostnames[2], false);
- createClusterHostPage.delete(hostnames[1]);
- createClusterHostPage.delete(hostnames[2]);
+ createClusterHostPage.remove(hostnames[1]);
+ createClusterHostPage.remove(hostnames[2]);
addHost(hostnames[3], false, true);
});
- it('should delete a host', () => {
- createClusterHostPage.delete(hostnames[1]);
+ it('should remove a host', () => {
+ createClusterHostPage.remove(hostnames[1]);
});
it('should add a host with some predefined labels and verify it', () => {
const hostnames = [
'ceph-node-00.cephlab.com',
'ceph-node-01.cephlab.com',
- 'ceph-node-02.cephlab.com'
+ 'ceph-node-02.cephlab.com',
+ 'ceph-node-03.cephlab.com'
];
beforeEach(() => {
});
it('should check if rgw service is running', () => {
- hosts.clickTab('cd-host-details', hostnames[1], 'Daemons');
+ hosts.clickTab('cd-host-details', hostnames[3], 'Daemons');
cy.get('cd-host-details').within(() => {
services.checkServiceStatus('rgw');
});
});
it('should force maintenance and exit', { retries: 1 }, () => {
- hosts.maintenance(hostnames[1], true, true);
+ hosts.maintenance(hostnames[3], true, true);
+ });
+
+ it('should drain, remove and add the host back', () => {
+ hosts.drain(hostnames[1]);
+ hosts.remove(hostnames[1]);
+ hosts.navigateTo('add');
+ hosts.add(hostnames[1]);
+ hosts.checkExist(hostnames[1], true);
});
});
OrchestratorFeature.HOST_ADD,
OrchestratorFeature.HOST_LABEL_ADD,
OrchestratorFeature.HOST_REMOVE,
- OrchestratorFeature.HOST_LABEL_REMOVE
+ OrchestratorFeature.HOST_LABEL_REMOVE,
+ OrchestratorFeature.HOST_DRAIN
];
await testTableActions(true, features, tests);
});
modalRef: NgbModalRef;
isExecuting = false;
errorMessage: string;
- enableButton: boolean;
+ enableMaintenanceBtn: boolean;
+ enableDrainBtn: boolean;
bsModalRef: NgbModalRef;
icons = Icons;
maintenance: [
OrchestratorFeature.HOST_MAINTENANCE_ENTER,
OrchestratorFeature.HOST_MAINTENANCE_EXIT
- ]
+ ],
+ drain: [OrchestratorFeature.HOST_DRAIN]
};
constructor(
click: () => this.editAction(),
disable: (selection: CdTableSelection) => this.getDisable('edit', selection)
},
+ {
+ name: this.actionLabels.START_DRAIN,
+ permission: 'update',
+ icon: Icons.exit,
+ click: () => this.hostDrain(),
+ disable: (selection: CdTableSelection) =>
+ this.getDisable('drain', selection) || !this.enableDrainBtn,
+ visible: () => !this.showGeneralActionsOnly && this.enableDrainBtn
+ },
+ {
+ name: this.actionLabels.STOP_DRAIN,
+ permission: 'update',
+ icon: Icons.exit,
+ click: () => this.hostDrain(true),
+ disable: (selection: CdTableSelection) =>
+ this.getDisable('drain', selection) || this.enableDrainBtn,
+ visible: () => !this.showGeneralActionsOnly && !this.enableDrainBtn
+ },
{
name: this.actionLabels.REMOVE,
permission: 'delete',
icon: Icons.enter,
click: () => this.hostMaintenance(),
disable: (selection: CdTableSelection) =>
- this.getDisable('maintenance', selection) || this.isExecuting || this.enableButton,
- visible: () => !this.showGeneralActionsOnly
+ this.getDisable('maintenance', selection) ||
+ this.isExecuting ||
+ this.enableMaintenanceBtn,
+ visible: () => !this.showGeneralActionsOnly && !this.enableMaintenanceBtn
},
{
name: this.actionLabels.EXIT_MAINTENANCE,
icon: Icons.exit,
click: () => this.hostMaintenance(),
disable: (selection: CdTableSelection) =>
- this.getDisable('maintenance', selection) || this.isExecuting || !this.enableButton,
- visible: () => !this.showGeneralActionsOnly
+ this.getDisable('maintenance', selection) ||
+ this.isExecuting ||
+ !this.enableMaintenanceBtn,
+ visible: () => !this.showGeneralActionsOnly && this.enableMaintenanceBtn
}
];
}
updateSelection(selection: CdTableSelection) {
this.selection = selection;
- this.enableButton = false;
+ this.enableMaintenanceBtn = false;
+ this.enableDrainBtn = false;
if (this.selection.hasSelection) {
if (this.selection.first().status === 'maintenance') {
- this.enableButton = true;
+ this.enableMaintenanceBtn = true;
+ }
+
+ if (!this.selection.first().labels.includes('_no_schedule')) {
+ this.enableDrainBtn = true;
}
}
}
}
}
+ hostDrain(stop = false) {
+ const host = this.selection.first();
+ if (stop) {
+ const index = host['labels'].indexOf('_no_schedule', 0);
+ host['labels'].splice(index, 1);
+ this.hostService.update(host['hostname'], true, host['labels']).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`"${host['hostname']}" stopped draining`
+ );
+ this.table.refreshBtn();
+ });
+ } else {
+ this.hostService.update(host['hostname'], false, [], false, false, true).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.info,
+ $localize`"${host['hostname']}" started draining`
+ );
+ this.table.refreshBtn();
+ });
+ }
+ }
+
getDisable(
- action: 'add' | 'edit' | 'remove' | 'maintenance',
+ action: 'add' | 'edit' | 'remove' | 'maintenance' | 'drain',
selection: CdTableSelection
): boolean | string {
- if (action === 'remove' || action === 'edit' || action === 'maintenance') {
+ if (
+ action === 'remove' ||
+ action === 'edit' ||
+ action === 'maintenance' ||
+ action === 'drain'
+ ) {
if (!selection?.hasSingleSelection) {
return true;
}
});
it('should update host', fakeAsync(() => {
- service.update('mon0', true, ['foo', 'bar']).subscribe();
+ service.update('mon0', true, ['foo', 'bar'], true, false).subscribe();
const req = httpTesting.expectOne('api/host/mon0');
expect(req.request.method).toBe('PUT');
expect(req.request.body).toEqual({
force: false,
labels: ['foo', 'bar'],
+ maintenance: true,
+ update_labels: true,
+ drain: false
+ });
+ }));
+
+ it('should test host drain call', fakeAsync(() => {
+ service.update('host0', false, null, false, false, true).subscribe();
+ const req = httpTesting.expectOne('api/host/host0');
+ expect(req.request.method).toBe('PUT');
+ expect(req.request.body).toEqual({
+ force: false,
+ labels: null,
maintenance: false,
- update_labels: true
+ update_labels: false,
+ drain: true
});
}));
updateLabels = false,
labels: string[] = [],
maintenance = false,
- force = false
+ force = false,
+ drain = false
) {
return this.http.put(
`${this.baseURL}/${hostname}`,
update_labels: updateLabels,
labels: labels,
maintenance: maintenance,
- force: force
+ force: force,
+ drain: drain
},
{ headers: { Accept: this.getVersionHeaderValue(0, 1) } }
);
FLAGS: string;
ENTER_MAINTENANCE: string;
EXIT_MAINTENANCE: string;
+ START_DRAIN: string;
+ STOP_DRAIN: string;
constructor() {
/* Create a new item */
this.FLAGS = $localize`Flags`;
this.ENTER_MAINTENANCE = $localize`Enter Maintenance`;
this.EXIT_MAINTENANCE = $localize`Exit Maintenance`;
+ this.START_DRAIN = $localize`Start Drain`;
+ this.STOP_DRAIN = $localize`Stop Drain`;
/* Prometheus wording */
this.RECREATE = $localize`Recreate`;
HOST_MAINTENANCE_ENTER = 'enter_host_maintenance',
HOST_MAINTENANCE_EXIT = 'exit_host_maintenance',
HOST_FACTS = 'get_facts',
+ HOST_DRAIN = 'drain_host',
SERVICE_LIST = 'describe_service',
SERVICE_CREATE = 'apply',
\ name of the host to be processed.\n :param update_labels: To update\
\ the labels.\n :param labels: List of labels.\n :param maintenance:\
\ Enter/Exit maintenance mode.\n :param force: Force enter maintenance\
- \ mode.\n "
+ \ mode.\n :param drain: Drain host\n "
parameters:
- description: Hostname
in: path
application/json:
schema:
properties:
+ drain:
+ default: false
+ description: Drain Host
+ type: boolean
force:
default: false
description: Force Enter Maintenance
def remove_label(self, host: str, label: str) -> OrchResult[str]:
return self.api.remove_host_label(host, label)
+ @wait_api_result
+ def drain(self, hostname: str):
+ return self.api.drain_host(hostname)
+
class InventoryManager(ResourceManager):
@wait_api_result
HOST_LABEL_REMOVE = 'remove_host_label'
HOST_MAINTENANCE_ENTER = 'enter_host_maintenance'
HOST_MAINTENANCE_EXIT = 'exit_host_maintenance'
+ HOST_DRAIN = 'drain_host'
SERVICE_LIST = 'describe_service'
SERVICE_CREATE = 'apply'
self._get(inventory_url)
self.assertStatus(503)
+ def test_host_drain(self):
+ mgr.list_servers.return_value = []
+ orch_hosts = [
+ HostSpec('node0')
+ ]
+ with patch_orch(True, hosts=orch_hosts):
+ self._put('{}/node0'.format(self.URL_HOST), {'drain': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(200)
+ self.assertHeader('Content-Type',
+ 'application/vnd.ceph.api.v0.1+json')
+
+ # maintenance without orchestrator service
+ with patch_orch(False):
+ self._put('{}/node0'.format(self.URL_HOST), {'drain': True},
+ version=APIVersion(0, 1))
+ self.assertStatus(503)
+
class HostUiControllerTest(ControllerTestCase):
URL_HOST = '/ui-api/host'