From: Nizamudeen A Date: Fri, 30 Apr 2021 18:29:38 +0000 (+0530) Subject: mgr/dashboard: Include Network address and labels on Host Creation form X-Git-Tag: v16.2.5~34^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=c4f4b02a6aca434f9d89eb1b8d9b93f2d89da691;p=ceph.git mgr/dashboard: Include Network address and labels on Host Creation form The ability to create host by specifying network address and also create labels. https://tracker.ceph.com/issues/50318 Signed-off-by: Nizamudeen A (cherry picked from commit 7c1df692f270ad0e7c80559e5b190c98eea44981) Conflicts: qa/tasks/mgr/dashboard/test_host.py - Keep the {'status': ''} parameter on the request body of _post src/pybind/mgr/dashboard/controllers/host.py - Added the addr and labels as parameter in the create() method as well as the add_host() method. src/pybind/mgr/dashboard/services/orchestrator.py - Added the addr and labels as parameter in the add() method and removed the status parameter src/pybind/mgr/dashboard/openapi.yaml - Regenerated the openapi.yaml spec --- diff --git a/qa/tasks/mgr/dashboard/test_host.py b/qa/tasks/mgr/dashboard/test_host.py index b642ad07e4ef..124fff8d1544 100644 --- a/qa/tasks/mgr/dashboard/test_host.py +++ b/qa/tasks/mgr/dashboard/test_host.py @@ -146,7 +146,7 @@ class HostControllerTest(DashboardTestCase): class HostControllerNoOrchestratorTest(DashboardTestCase): def test_host_create(self): - self._post('/api/host?hostname=foo', {'status': ''}) + self._post('/api/host?hostname=foo', {'status': ''}, version='0.1') self.assertStatus(503) self.assertError(code='orchestrator_status_unavailable', component='orchestrator') diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index 3efcccae2681..a13b1eb20f6a 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -256,11 +256,13 @@ def get_inventories(hosts: Optional[List[str]] = None, @allow_empty_body -def add_host(hostname: str, status: Optional[str] = None): +def add_host(hostname: str, addr: Optional[str] = None, + labels: Optional[List[str]] = None, + status: Optional[str] = None): orch_client = OrchClient.instance() host = Host() host.check_orchestrator_host_op(orch_client, hostname) - orch_client.hosts.add(hostname) + orch_client.hosts.add(hostname, addr, labels) if status == 'maintenance': orch_client.hosts.enter_maintenance(hostname) @@ -287,12 +289,17 @@ class Host(RESTController): @EndpointDoc('', parameters={ 'hostname': (str, 'Hostname'), - 'status': (str, 'Host Status') + 'addr': (str, 'Network Address'), + 'labels': ([str], 'Host Labels'), + 'status': (str, 'Host Status'), }, responses={200: None, 204: None}) + @RESTController.MethodMap(version='0.1') def create(self, hostname: str, + addr: Optional[str] = None, + labels: Optional[List[str]] = None, status: Optional[str] = None): # pragma: no cover - requires realtime env - add_host(hostname, status) + add_host(hostname, addr, labels, status) @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE]) @handle_orchestrator_error('host') @@ -400,6 +407,7 @@ class Host(RESTController): 'force': (bool, 'Force Enter Maintenance') }, responses={200: None, 204: None}) + @RESTController.MethodMap(version='0.1') def set(self, hostname: str, update_labels: bool = False, labels: List[str] = None, maintenance: bool = False, force: bool = False): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html index ef7c293ea3f4..2296d7ddae84 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html @@ -32,6 +32,38 @@ + +
+ +
+ + The value is not a valid IP address. +
+
+ + +
+ +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts index 5e722bd3dcd4..dbb834ea8c82 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts @@ -1,5 +1,5 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; @@ -7,12 +7,13 @@ import { ToastrModule } from 'ngx-toastr'; import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component'; import { SharedModule } from '~/app/shared/shared.module'; -import { configureTestBed } from '~/testing/unit-test-helper'; +import { configureTestBed, FormHelper } from '~/testing/unit-test-helper'; import { HostFormComponent } from './host-form.component'; describe('HostFormComponent', () => { let component: HostFormComponent; let fixture: ComponentFixture; + let formHelper: FormHelper; configureTestBed( { @@ -31,6 +32,7 @@ describe('HostFormComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(HostFormComponent); component = fixture.componentInstance; + formHelper = new FormHelper(component.hostForm); fixture.detectChanges(); }); @@ -38,6 +40,33 @@ describe('HostFormComponent', () => { expect(component).toBeTruthy(); }); + it('should validate the network address is valid', fakeAsync(() => { + formHelper.setValue('addr', '115.42.150.37', true); + tick(); + formHelper.expectValid('addr'); + })); + + it('should show error if network address is invalid', fakeAsync(() => { + formHelper.setValue('addr', '666.10.10.20', true); + tick(); + formHelper.expectError('addr', 'pattern'); + })); + + it('should submit the network address', () => { + component.hostForm.get('addr').setValue('127.0.0.1'); + fixture.detectChanges(); + component.submit(); + expect(component.addr).toBe('127.0.0.1'); + }); + + it('should validate the labels are added', () => { + const labels = ['label1', 'label2']; + component.hostForm.get('labels').patchValue(labels); + fixture.detectChanges(); + component.submit(); + expect(component.allLabels).toBe(labels); + }); + it('should select maintenance mode', () => { component.hostForm.get('maintenance').setValue(true); fixture.detectChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts index e9e51c3e1ebb..b90312ff855f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts @@ -3,6 +3,7 @@ import { FormControl, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { HostService } from '~/app/shared/api/host.service'; +import { SelectMessages } from '~/app/shared/components/select/select-messages.model'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { CdForm } from '~/app/shared/forms/cd-form'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; @@ -20,7 +21,15 @@ export class HostFormComponent extends CdForm implements OnInit { action: string; resource: string; hostnames: string[]; + addr: string; status: string; + allLabels: any; + + messages = new SelectMessages({ + empty: $localize`There are no labels.`, + filter: $localize`Filter or add labels`, + add: $localize`Add label` + }); constructor( private router: Router, @@ -53,19 +62,25 @@ export class HostFormComponent extends CdForm implements OnInit { }) ] }), + addr: new FormControl('', { + validators: [CdValidators.ip()] + }), + labels: new FormControl([]), maintenance: new FormControl(false) }); } submit() { const hostname = this.hostForm.get('hostname').value; + this.addr = this.hostForm.get('addr').value; this.status = this.hostForm.get('maintenance').value ? 'maintenance' : ''; + this.allLabels = this.hostForm.get('labels').value; this.taskWrapper .wrapTaskAroundCall({ task: new FinishedTask('host/' + URLVerbs.CREATE, { hostname: hostname }), - call: this.hostService.create(hostname, this.status) + call: this.hostService.create(hostname, this.addr, this.allLabels, this.status) }) .subscribe({ error: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts index ba2b4aded7ef..7f8956baa263 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts @@ -25,11 +25,11 @@ export class HostService { return this.http.get(this.baseURL); } - create(hostname: string, status: string) { + create(hostname: string, addr: string, labels: string[], status: string) { return this.http.post( this.baseURL, - { hostname: hostname, status: status }, - { observe: 'response' } + { hostname: hostname, addr: addr, labels: labels, status: status }, + { observe: 'response', headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } } ); } @@ -62,12 +62,16 @@ export class HostService { maintenance = false, force = false ) { - return this.http.put(`${this.baseURL}/${hostname}`, { - update_labels: updateLabels, - labels: labels, - maintenance: maintenance, - force: force - }); + return this.http.put( + `${this.baseURL}/${hostname}`, + { + update_labels: updateLabels, + labels: labels, + maintenance: maintenance, + force: force + }, + { headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } } + ); } identifyDevice(hostname: string, device: string, duration: number) { diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 20d544b6bf1a..921dadb0471d 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -3239,9 +3239,17 @@ paths: application/json: schema: properties: + addr: + description: Network Address + type: string hostname: description: Hostname type: string + labels: + description: Host Labels + items: + type: string + type: array status: description: Host Status type: string @@ -3251,12 +3259,12 @@ paths: responses: '201': content: - application/vnd.ceph.api.v1.0+json: + application/vnd.ceph.api.v0.1+json: type: object description: Resource created. '202': content: - application/vnd.ceph.api.v1.0+json: + application/vnd.ceph.api.v0.1+json: type: object description: Operation is still executing. Please check the task queue. '400': @@ -3372,14 +3380,14 @@ paths: responses: '200': content: - application/vnd.ceph.api.v1.0+json: + application/vnd.ceph.api.v0.1+json: schema: properties: {} type: object description: Resource updated. '202': content: - application/vnd.ceph.api.v1.0+json: + application/vnd.ceph.api.v0.1+json: type: object description: Operation is still executing. Please check the task queue. '400': diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py index 1b472932c933..8891e8e3d3c6 100644 --- a/src/pybind/mgr/dashboard/services/orchestrator.py +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -64,8 +64,8 @@ class HostManger(ResourceManager): return hosts[0] if hosts else None @wait_api_result - def add(self, hostname: str): - return self.api.add_host(HostSpec(hostname)) + def add(self, hostname: str, addr: str, labels: List[str]): + return self.api.add_host(HostSpec(hostname, addr=addr, labels=labels)) @wait_api_result def remove(self, hostname: str): diff --git a/src/pybind/mgr/dashboard/tests/test_host.py b/src/pybind/mgr/dashboard/tests/test_host.py index 93b543ee4a92..6d719a0fc922 100644 --- a/src/pybind/mgr/dashboard/tests/test_host.py +++ b/src/pybind/mgr/dashboard/tests/test_host.py @@ -113,6 +113,8 @@ class HostControllerTest(ControllerTestCase): self._get('{}/node1'.format(self.URL_HOST)) self.assertStatus(200) self.assertIn('labels', self.json_body()) + self.assertIn('status', self.json_body()) + self.assertIn('addr', self.json_body()) def test_get_3(self): mgr.list_servers.return_value = [] @@ -121,15 +123,19 @@ class HostControllerTest(ControllerTestCase): self._get('{}/node1'.format(self.URL_HOST)) self.assertStatus(200) self.assertIn('labels', self.json_body()) + self.assertIn('status', self.json_body()) + self.assertIn('addr', self.json_body()) @mock.patch('dashboard.controllers.host.add_host') def test_add_host(self, mock_add_host): with patch_orch(True): payload = { 'hostname': 'node0', + 'addr': '192.0.2.0', + 'labels': 'mon', 'status': 'maintenance' } - self._post(self.URL_HOST, payload) + self._post(self.URL_HOST, payload, version='0.1') self.assertStatus(201) mock_add_host.assert_called() @@ -143,14 +149,16 @@ class HostControllerTest(ControllerTestCase): fake_client.hosts.add_label = mock.Mock() payload = {'update_labels': True, 'labels': ['bbb', 'ccc']} - self._put('{}/node0'.format(self.URL_HOST), payload) + self._put('{}/node0'.format(self.URL_HOST), payload, version='0.1') self.assertStatus(200) + self.assertHeader('Content-Type', + 'application/vnd.ceph.api.v0.1+json') fake_client.hosts.remove_label.assert_called_once_with('node0', 'aaa') fake_client.hosts.add_label.assert_called_once_with('node0', 'ccc') # return 400 if type other than List[str] self._put('{}/node0'.format(self.URL_HOST), {'update_labels': True, - 'labels': 'ddd'}) + 'labels': 'ddd'}, version='0.1') self.assertStatus(400) def test_host_maintenance(self): @@ -161,22 +169,25 @@ class HostControllerTest(ControllerTestCase): ] with patch_orch(True, hosts=orch_hosts): # enter maintenance mode - self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}) + self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, version='0.1') self.assertStatus(200) + self.assertHeader('Content-Type', + 'application/vnd.ceph.api.v0.1+json') # force enter maintenance mode - self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True}) + self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True, 'force': True}, + version='0.1') self.assertStatus(200) # exit maintenance mode - self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}) + self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, version='0.1') self.assertStatus(200) - self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True}) + self._put('{}/node1'.format(self.URL_HOST), {'maintenance': True}, version='0.1') self.assertStatus(200) # maintenance without orchestrator service with patch_orch(False): - self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}) + self._put('{}/node0'.format(self.URL_HOST), {'maintenance': True}, version='0.1') self.assertStatus(503) @mock.patch('dashboard.controllers.host.time')