]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Include Network address and labels on Host Creation form 42027/head
authorNizamudeen A <nia@redhat.com>
Fri, 30 Apr 2021 18:29:38 +0000 (23:59 +0530)
committerNizamudeen A <nia@redhat.com>
Tue, 29 Jun 2021 08:51:46 +0000 (14:21 +0530)
The ability to create host by specifying network address and also create
labels.

https://tracker.ceph.com/issues/50318
Signed-off-by: Nizamudeen A <nia@redhat.com>
(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

qa/tasks/mgr/dashboard/test_host.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/orchestrator.py
src/pybind/mgr/dashboard/tests/test_host.py

index b642ad07e4ef3a5f621a3756153acf58c2eacc0f..124fff8d1544f9d5b672af75b6115d09f2f3658c 100644 (file)
@@ -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')
index 3efcccae2681125d40a714f30cd5103119302648..a13b1eb20f6a3754c141c5c8eb56a10344331729 100644 (file)
@@ -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):
index ef7c293ea3f43e7954425963e7be898e2e9fddc0..2296d7ddae847db80191672d4a83d11315fa6fce 100644 (file)
           </div>
         </div>
 
+        <!-- Address -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="addr"
+                 i18n>Nework address</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="192.168.0.1"
+                   id="addr"
+                   name="addr"
+                   formControlName="addr">
+            <span class="invalid-feedback"
+                  *ngIf="hostForm.showError('addr', formDir, 'pattern')"
+                  i18n>The value is not a valid IP address.</span>
+          </div>
+        </div>
+
+        <!-- Labels -->
+        <div class="form-group row">
+          <label i18n
+                 for="labels"
+                 class="cd-col-form-label">Labels</label>
+          <div class="cd-col-form-input">
+            <cd-select-badges id="labels"
+                              [data]="hostForm.controls.labels.value"
+                              [customBadges]="true"
+                              [messages]="messages">
+            </cd-select-badges>
+          </div>
+        </div>
+
         <!-- Maintenance Mode -->
         <div class="form-group row">
           <div class="cd-col-form-offset">
index 5e722bd3dcd405992e46fc5852e4dc267e387567..dbb834ea8c82cc7fa37ef6b0ac829638e09386db 100644 (file)
@@ -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<HostFormComponent>;
+  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();
index e9e51c3e1ebbc10631c530c56776106a6189a608..b90312ff855f2f305904961cea58fc9139ffdae4 100644 (file)
@@ -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: () => {
index ba2b4aded7efa282ceae545374a493ebdbc524f3..7f8956baa263d1919219e475b87088d05b408384 100644 (file)
@@ -25,11 +25,11 @@ export class HostService {
     return this.http.get<object[]>(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) {
index 20d544b6bf1ae9ac28b09ec2bd29d0418cd0a08f..921dadb0471d01011c3647591d0eb1d12aa587d3 100644 (file)
@@ -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':
index 1b472932c933cfac7721cff9f7091bcf7bb9d01e..8891e8e3d3c6bcf46fcda63eec181e971ea820dd 100644 (file)
@@ -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):
index 93b543ee4a922bf22b2c3e067525b38076a7daeb..6d719a0fc92218052c64f36e9a1927ef5da95932 100644 (file)
@@ -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')