From 7c1df692f270ad0e7c80559e5b190c98eea44981 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Fri, 30 Apr 2021 23:59:38 +0530 Subject: [PATCH] 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 --- qa/tasks/mgr/dashboard/test_host.py | 2 +- src/pybind/mgr/dashboard/controllers/host.py | 14 +++++++- .../hosts/host-form/host-form.component.html | 32 ++++++++++++++++++ .../host-form/host-form.component.spec.ts | 33 +++++++++++++++++-- .../hosts/host-form/host-form.component.ts | 17 +++++++++- .../src/app/shared/api/host.service.ts | 22 ++++++++----- src/pybind/mgr/dashboard/openapi.yaml | 18 +++++++--- .../mgr/dashboard/services/orchestrator.py | 4 +-- src/pybind/mgr/dashboard/tests/test_host.py | 23 +++++++++---- 9 files changed, 138 insertions(+), 27 deletions(-) diff --git a/qa/tasks/mgr/dashboard/test_host.py b/qa/tasks/mgr/dashboard/test_host.py index 49bb33533cd71..935e9d71314df 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') + self._post('/api/host?hostname=foo', 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 a40890602b92a..d27da0a7aa17f 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -273,11 +273,22 @@ class Host(RESTController): @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_CREATE]) @handle_orchestrator_error('host') @host_task('create', {'hostname': '{hostname}'}) + @EndpointDoc('', + parameters={ + 'hostname': (str, 'Hostname'), + 'addr': (str, 'Network Address'), + 'labels': ([str], 'Host Labels'), + 'status': (str, 'Status of the Host') + }, + 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 orch_client = OrchClient.instance() self._check_orchestrator_host_op(orch_client, hostname, True) - orch_client.hosts.add(hostname, status) + orch_client.hosts.add(hostname, addr, labels, status) create._cp_config = {'tools.json_in.force': False} # pylint: disable=W0212 @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE]) @@ -387,6 +398,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 ef7c293ea3f43..2296d7ddae847 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 d9135197e85aa..fad0fa7270fc4 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('maintenance'); 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 e9e51c3e1ebbc..b90312ff855f2 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 ba2b4aded7efa..df643afb0ee56 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, 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 ba2b26bf0dbe8..7973832c73282 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -3239,9 +3239,19 @@ 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: Status of the Host type: string required: - hostname @@ -3249,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': @@ -3370,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 45b0d32309268..d564c46994080 100644 --- a/src/pybind/mgr/dashboard/services/orchestrator.py +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -63,8 +63,8 @@ class HostManger(ResourceManager): return hosts[0] if hosts else None @wait_api_result - def add(self, hostname: str, status: str): - return self.api.add_host(HostSpec(hostname, status=status)) + def add(self, hostname: str, addr: str, labels: List[str], status: str): + return self.api.add_host(HostSpec(hostname, addr=addr, labels=labels, status=status)) @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 6093ba8f4314b..e7a09a3a71074 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,6 +123,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_set_labels(self): mgr.list_servers.return_value = [] @@ -132,14 +136,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): @@ -150,22 +156,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') -- 2.39.5