]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add actions to create, edit and delete smb join-auth and usersgroups... 61315/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Fri, 10 Jan 2025 09:23:37 +0000 (10:23 +0100)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Thu, 20 Feb 2025 08:05:16 +0000 (09:05 +0100)
Add join-auth and usersgroups resources management and improve the way to select those from the smb cluster form using a dropdown.
Add option to navigate to join-auth/usersgroups resource create form from smb form
Add some additional fixes left over from previous work, such as adding helper texts or adding missing smb cluster form fields

Fixes: https://tracker.ceph.com/issues/69483
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
32 files changed:
src/pybind/mgr/dashboard/controllers/smb.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-list/smb-join-auth-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-tabs/smb-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-list/smb-usersgroups-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/tests/test_smb.py

index 7a1390f151b7c59ad91da13d62aaaa9f8d3d57d1..9020d4cbd194626c3e47ce5fc178b73a638f5af9 100644 (file)
@@ -1,6 +1,5 @@
 
 # -*- coding: utf-8 -*-
-
 import json
 import logging
 from functools import wraps
@@ -8,7 +7,7 @@ from typing import List
 
 from smb.enums import Intent
 from smb.proto import Simplified
-from smb.resources import Cluster, Share
+from smb.resources import Cluster, JoinAuth, Share, UsersAndGroups
 
 from dashboard.controllers._docs import EndpointDoc
 from dashboard.controllers._permissions import CreatePermission, DeletePermission
@@ -42,35 +41,6 @@ CLUSTER_SCHEMA = {
     }, "Placement configuration for the resource")
 }
 
-CLUSTER_SCHEMA_RESULTS = {
-    "results": ([{
-        "resource": ({
-            "resource_type": (str, "ceph.smb.cluster"),
-            "cluster_id": (str, "Unique identifier for the cluster"),
-            "auth_mode": (str, "Either 'active-directory' or 'user'"),
-            "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"),
-            "domain_settings": ({
-                "realm": (str, "Domain realm, e.g., 'DOMAIN1.SINK.TEST'"),
-                "join_sources": ([{
-                    "source_type": (str, "resource"),
-                    "ref": (str, "Reference identifier for the join auth resource")
-                }], "List of join auth sources for domain settings")
-            }, "Domain-specific settings for active-directory auth mode"),
-            "user_group_settings": ([{
-                "source_type": (str, "resource"),
-                "ref": (str, "Reference identifier for the user group resource")
-            }], "User group settings for user auth mode (optional)"),
-            "custom_dns": ([str], "List of custom DNS server addresses (optional)"),
-            "placement": ({
-                "count": (int, "Number of instances to place")
-            }, "Placement configuration for the resource (optional)"),
-        }, "Resource details"),
-        "state": (str, "State of the resource"),
-        "success": (bool, "Indicates whether the operation was successful")
-    }], "List of results with resource details"),
-    "success": (bool, "Overall success status of the operation")
-}
-
 LIST_CLUSTER_SCHEMA = [CLUSTER_SCHEMA]
 
 SHARE_SCHEMA = {
@@ -125,29 +95,26 @@ USERSGROUPS_SCHEMA = {
 
 LIST_USERSGROUPS_SCHEMA = [USERSGROUPS_SCHEMA]
 
-SHARE_SCHEMA_RESULTS = {
-    "results": ([{
-        "resource": ({
-            "resource_type": (str, "ceph.smb.share"),
-            "cluster_id": (str, "Unique identifier for the cluster"),
-            "share_id": (str, "Unique identifier for the share"),
-            "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"),
-            "name": (str, "Name of the share"),
-            "readonly": (bool, "Indicates if the share is read-only"),
-            "browseable": (bool, "Indicates if the share is browseable"),
-            "cephfs": ({
-                "volume": (str, "Name of the CephFS file system"),
-                "path": (str, "Path within the CephFS file system"),
-                "subvolumegroup": (str, "Subvolume Group in CephFS file system"),
-                "subvolume": (str, "Subvolume within the CephFS file system"),
-                "provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'")
-            }, "Configuration for the CephFS share")
-        }, "Resource details"),
-        "state": (str, "State of the resource"),
-        "success": (bool, "Indicates whether the operation was successful")
-    }], "List of results with resource details"),
-    "success": (bool, "Overall success status of the operation")
-}
+
+def add_results_to_schema(schema):
+
+    results_field = {
+        "results": ([{
+            "resource": (schema, "Resource"),
+            "state": (str, "The current state of the resource,\
+                        e.g., 'created', 'updated', 'deleted'"),
+            "success": (bool, "Indicates if the operation was successful"),
+        }], "List of operation results"),
+        "success": (bool, "Indicates if the overall operation was successful")
+    }
+
+    return results_field
+
+
+CLUSTER_SCHEMA_RESULTS = add_results_to_schema(CLUSTER_SCHEMA)
+SHARE_SCHEMA_RESULTS = add_results_to_schema(SHARE_SCHEMA)
+JOIN_AUTH_SCHEMA_RESULTS = add_results_to_schema(JOIN_AUTH_SCHEMA)
+USERSGROUPS_SCHEMA_RESULTS = add_results_to_schema(JOIN_AUTH_SCHEMA_RESULTS)
 
 
 def raise_on_failure(func):
@@ -319,7 +286,7 @@ class SMBJoinAuth(RESTController):
     @ReadPermission
     @EndpointDoc("List smb join authorization resources",
                  responses={200: LIST_JOIN_AUTH_SCHEMA})
-    def list(self, join_auth: str = '') -> List[Share]:
+    def list(self) -> List[JoinAuth]:
         """
         List all smb join auth resources
 
@@ -329,9 +296,61 @@ class SMBJoinAuth(RESTController):
         res = mgr.remote(
             'smb',
             'show',
-            [f'{self._resource}.{join_auth}' if join_auth else self._resource])
+            [self._resource])
         return res['resources'] if 'resources' in res else [res]
 
+    @ReadPermission
+    @EndpointDoc("Get smb join authorization resource",
+                 responses={200: JOIN_AUTH_SCHEMA})
+    def get(self, auth_id: str) -> JoinAuth:
+        """
+        Get Join auth resource
+
+        :return: Returns join auth.
+        :rtype: Dict
+        """
+        res = mgr.remote(
+            'smb',
+            'show',
+            [f'{self._resource}.{auth_id}'])
+        return res['resources'] if 'resources' in res else res
+
+    @CreatePermission
+    @EndpointDoc("Create smb join auth",
+                 parameters={
+                     'auth_id': (str, 'auth_id'),
+                     'username': (str, 'username'),
+                     'password': (str, 'password')
+                 },
+                 responses={201: JOIN_AUTH_SCHEMA_RESULTS})
+    def create(self, join_auth: JoinAuth) -> Simplified:
+        """
+        Create smb join auth resource
+
+        :return: Returns join auth resource.
+        :rtype: Dict
+        """
+        return mgr.remote('smb', 'apply_resources', json.dumps(join_auth)).to_simplified()
+
+    @CreatePermission
+    @EndpointDoc("Delete smb join auth",
+                 parameters={
+                     'auth_id': (str, 'auth_id')
+                 },
+                 responses={204: None})
+    def delete(self, auth_id: str) -> None:
+        """
+        Delete smb join auth resource
+
+        :param auth_id: Join Auth identifier
+        :return: None.
+        """
+        resource = {}
+        resource['resource_type'] = self._resource
+        resource['auth_id'] = auth_id
+        resource['intent'] = Intent.REMOVED
+        return mgr.remote('smb', 'apply_resources', json.dumps(resource)).one().to_simplified()
+
 
 @APIRouter('/smb/usersgroups', Scope.SMB)
 @APIDoc("SMB Users Groups API", "SMB")
@@ -341,19 +360,71 @@ class SMBUsersgroups(RESTController):
     @ReadPermission
     @EndpointDoc("List smb user resources",
                  responses={200: LIST_USERSGROUPS_SCHEMA})
-    def list(self, users_groups: str = '') -> List[Share]:
+    def list(self) -> List[UsersAndGroups]:
         """
         List all smb usersgroups resources
 
-        :return: Returns list of usersgroups.
+        :return: Returns list of usersgroups
         :rtype: List[Dict]
         """
         res = mgr.remote(
             'smb',
             'show',
-            [f'{self._resource}.{users_groups}' if users_groups else self._resource])
+            [self._resource])
         return res['resources'] if 'resources' in res else [res]
 
+    @ReadPermission
+    @EndpointDoc("Get smb usersgroups authorization resource",
+                 responses={200: USERSGROUPS_SCHEMA})
+    def get(self, users_groups_id: str) -> UsersAndGroups:
+        """
+        Get Users and groups resource
+
+        :return: Returns join auth.
+        :rtype: Dict
+        """
+        res = mgr.remote(
+            'smb',
+            'show',
+            [f'{self._resource}.{users_groups_id}'])
+        return res['resources'] if 'resources' in res else res
+
+    @CreatePermission
+    @EndpointDoc("Create smb usersgroups",
+                 parameters={
+                     'users_groups_id': (str, 'users_groups_id'),
+                     'username': (str, 'username'),
+                     'password': (str, 'password')
+                 },
+                 responses={201: USERSGROUPS_SCHEMA_RESULTS})
+    def create(self, usersgroups: UsersAndGroups) -> Simplified:
+        """
+        Create smb usersgroups resource
+
+        :return: Returns usersgroups resource.
+        :rtype: Dict
+        """
+        return mgr.remote('smb', 'apply_resources', json.dumps(usersgroups)).to_simplified()
+
+    @CreatePermission
+    @EndpointDoc("Delete smb join auth",
+                 parameters={
+                     'users_groups_id': (str, 'users_groups_id')
+                 },
+                 responses={204: None})
+    def delete(self, users_groups_id: str) -> None:
+        """
+        Delete smb usersgroups resource
+
+        :param users_group_id: Users  identifier
+        :return: None.
+        """
+        resource = {}
+        resource['resource_type'] = self._resource
+        resource['users_groups_id'] = users_groups_id
+        resource['intent'] = Intent.REMOVED
+        return mgr.remote('smb', 'apply_resources', json.dumps(resource)).one().to_simplified()
+
 
 @UIRouter('/smb')
 class SMBStatus(RESTController):
index 7fb568cad79bee3f50b19f0200018cc3e762e312..c755dd4f871fe268356590b7b57c15b8004c8d4f 100644 (file)
@@ -54,6 +54,8 @@ import { MultiClusterDetailsComponent } from './ceph/cluster/multi-cluster/multi
 import { SmbClusterFormComponent } from './ceph/smb/smb-cluster-form/smb-cluster-form.component';
 import { SmbTabsComponent } from './ceph/smb/smb-tabs/smb-tabs.component';
 import { SmbShareFormComponent } from './ceph/smb/smb-share-form/smb-share-form.component';
+import { SmbJoinAuthFormComponent } from './ceph/smb/smb-join-auth-form/smb-join-auth-form.component';
+import { SmbUsersgroupsFormComponent } from './ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component';
 
 @Injectable()
 export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
@@ -457,6 +459,26 @@ const routes: Routes = [
                 path: `share/${URLVerbs.CREATE}/:clusterId`,
                 component: SmbShareFormComponent,
                 data: { breadcrumbs: ActionLabels.CREATE }
+              },
+              {
+                path: `ad/${URLVerbs.CREATE}`,
+                component: SmbJoinAuthFormComponent,
+                data: { breadcrumbs: ActionLabels.CREATE }
+              },
+              {
+                path: `ad/${URLVerbs.EDIT}/:authId`,
+                component: SmbJoinAuthFormComponent,
+                data: { breadcrumbs: ActionLabels.EDIT }
+              },
+              {
+                path: `standalone/${URLVerbs.CREATE}`,
+                component: SmbUsersgroupsFormComponent,
+                data: { breadcrumbs: ActionLabels.CREATE }
+              },
+              {
+                path: `standalone/${URLVerbs.EDIT}/:usersGroupsId`,
+                component: SmbUsersgroupsFormComponent,
+                data: { breadcrumbs: ActionLabels.EDIT }
               }
             ]
           }
index 55f957c59c6c1c8b025250a0d1b3f9452131e6b7..da530d27dc1c56f6c1171049c0ec332461654495 100644 (file)
@@ -51,6 +51,7 @@ import {
 } from 'carbon-components-angular';
 
 import AddIcon from '@carbon/icons/es/add/32';
+import LaunchIcon from '@carbon/icons/es/launch/32';
 import Close from '@carbon/icons/es/close/32';
 import Trash from '@carbon/icons/es/trash-can/32';
 
@@ -109,6 +110,6 @@ import Trash from '@carbon/icons/es/trash-can/32';
 })
 export class CephfsModule {
   constructor(private iconService: IconService) {
-    this.iconService.registerAll([AddIcon, Close, Trash]);
+    this.iconService.registerAll([AddIcon, LaunchIcon, Close, Trash]);
   }
 }
index 887727c04926f20ef71b59340f336238aa29e068..d21972ce0ec08fc9f50fef590e5d473ca193e746 100644 (file)
@@ -15,7 +15,7 @@
       <cds-text-label
         labelInputID="cluster_id"
         i18n
-        helperText="Unique cluster identifier"
+        helperText="Unique identifier"
         i18n-helperText
         cdRequiredField="Cluster Name"
         [invalid]="smbForm.controls.cluster_id.invalid && smbForm.controls.cluster_id.dirty"
@@ -24,8 +24,6 @@
         <input
           cdsText
           type="text"
-          placeholder="Cluster Name..."
-          i18n-placeholder
           id="cluster_id"
           formControlName="cluster_id"
           [invalid]="smbForm.controls.cluster_id.invalid && smbForm.controls.cluster_id.dirty"
            class="d-flex">
         <cds-text-label labelInputID="domain_settings"
                         i18n
-                        cdRequiredField="Domain Settings">Domain Settings
+                        cdRequiredField="Domain Settings">Active Directory (AD) Settings
           <div class="cds-input-group">
             <input
               cdsText
               type="text"
-              placeholder="Domain Settings..."
-              i18n-placeholder
               id="domain_settings"
               formControlName="domain_settings"
               [value]="domainSettingsObject?.realm"
           smbForm.get('domain_settings').hasError('required') &&
           smbForm.controls.domain_settings.touched
         "
-        i18n>Specify the Realm and Join Sources in the Domain Settings field.</span
+        i18n>Specify the Realm and AD access resources in the Domain Settings field.</span
       >
       <div></div>
     </div>
 
     <!-- User Group Settings -->
     <ng-container formArrayName="joinSources"
-                  *ngFor="let dns of joinSources.controls; index as i">
+                  *ngFor="let _ of joinSources.controls; index as i">
       <div
         cdsRow
-        *ngIf="this.smbForm.get('auth_mode').value === 'user'"
+        *ngIf="this.smbForm.get('auth_mode').value === 'user' && usersGroups$ | async as usersGroups"
         class="form-item form-item-append"
       >
-        <div cdsCol
-             [columnNumbers]="{ lg: 14 }">
-          <cds-text-label for="joinSources"
-                          i18n
-                          cdRequiredField="User Group Id">User Group Id
-            <input
-              cdsText
-              type="text"
-              placeholder="User Group Id"
-              i18n-placeholder
-              [id]="'joinSources-' + i"
-              [formControlName]="i"
-              [invalid]="
-                smbForm.controls['joinSources'].controls[i].invalid &&
-                smbForm.controls['joinSources'].dirty
-              "
-            />
-          </cds-text-label>
-          <ng-template #refError>
+        <div
+          cdsCol
+          [columnNumbers]="{ lg: 14 }"
+        >
+          <cds-select
+            label="Standalone user access resources"
+            i18n-label
+            [formControlName]="i"
+            [invalid]="smbForm.controls.joinSources.controls[i].invalid && smbForm.controls.joinSources.controls[i].dirty"
+            [invalidText]="ugError"
+          >
+            <option
+              [value]="null"
+              i18n
+            >-- List of users and groups access resources --
+            </option>
+            <option *ngFor="let ug of usersGroups"
+                    [value]="ug.users_groups_id">{{ ug.users_groups_id }}</option>
+          </cds-select>
+          <ng-template #ugError>
             <span
               class="invalid-feedback"
-              *ngIf="smbForm.showError('joinSources[i]', formDir, 'required')"
               i18n
-              >This field is required.</span
-            >
+            >This field is required.</span>
           </ng-template>
         </div>
-        <div cdsCol
-             [columnNumbers]="{ lg: 1 }">
+        <div
+          cdsCol
+          [columnNumbers]="{ lg: 1 }"
+          class="item-action-btn spacing"
+        >
           <cds-icon-button
             kind="danger"
             *ngIf="i > 0"
             size="sm"
             (click)="removeUserGroupSetting(i)"
           >
-            <svg cdsIcon="trash-can"
-                 size="32"
-                 class="cds--btn__icon"></svg>
+            <svg
+              cdsIcon="trash-can"
+              size="32"
+              class="cds--btn__icon"></svg>
           </cds-icon-button>
         </div>
       </div>
               type="button"
               (click)="addUserGroupSetting()"
               i18n>
-        Add User Group Id
+        Add user group
         <svg cdsIcon="add"
              size="32"
              class="cds--btn__icon"
              icon></svg>
       </button>
+
+      <button
+        cdsButton="tertiary"
+        type="button"
+        (click)="navigateCreateUsersGroups()"
+        i18n
+      >
+        Create user group
+      <svg
+        cdsIcon="launch"
+        size="32"
+        class="cds--btn__icon"></svg>
+      </button>
     </div>
 
     <!-- Placement -->
       ></cds-number>
     </div>
 
-    <!-- Clustering -->
-    <div class="form-item">
-      <cds-select
-        formControlName="clustering"
-        for="clustering"
-        label="Clustering"
-        id="clustering"
-        helperText="Control if a cluster abstraction actually uses Samba’s clustering mechanism."
-        i18n-helperText
-      >
-        <option *ngFor="let data of allClustering"
-                i18n>{{ data | upperFirst }}</option>
-      </cds-select>
-    </div>
-
     <!-- Custom DNS -->
     <ng-container formArrayName="custom_dns"
-                  *ngFor="let dns of custom_dns.controls; index as i">
+                  *ngFor="let _ of custom_dns.controls; index as i">
       <div cdsRow
            class="form-item form-item-append">
         <div cdsCol
              [columnNumbers]="{ lg: 14 }">
+          <cds-text-label
+             for="custom_dns"
+             i18n
+           >DNS
           <input cdsText
                  [formControlName]="i"
-                 placeholder="Custom DNS"/>
+                 placeholder="192.168.76.204"/>
+          </cds-text-label>
         </div>
-        <div cdsCol
-             [columnNumbers]="{ lg: 1 }">
-          <cds-icon-button kind="danger"
-                           size="sm"
-                           (click)="removeCustomDNS(i)">
-            <svg cdsIcon="trash-can"
-                 size="32"
-                 class="cds--btn__icon"></svg>
+        <div
+          cdsCol
+          [columnNumbers]="{ lg: 1 }"
+          class="item-action-btn spacing"
+        >
+          <cds-icon-button
+            kind="danger"
+            size="sm"
+            (click)="removeCustomDNS(i)"
+          >
+            <svg
+              cdsIcon="trash-can"
+              size="32"
+              class="cds--btn__icon"
+            >
+            </svg>
           </cds-icon-button>
         </div>
       </div>
               type="button"
               (click)="addCustomDns()"
               i18n>
-        Add Custom DNS
-        <svg cdsIcon="add"
-             size="32"
-             class="cds--btn__icon"
-             icon></svg>
+        Add custom DNS
+        <svg
+          cdsIcon="add"
+          size="32"
+          class="cds--btn__icon"
+          icon></svg>
+      </button>
+      <cd-helper i18n>One or more IP Addresses that will be
+        applied to the Samba containers to override
+        the default DNS resolver(s). This option is
+        intended to be used when the host Ceph node
+        is not configured to resolve DNS entries within
+        AD domain(s).
+      </cd-helper>
+    </div>
+
+    <!-- Clustering -->
+    <div class="form-item">
+      <cds-select
+        formControlName="clustering"
+        for="clustering"
+        label="Clustering"
+        id="clustering"
+        helperText="Default value indicates that clustering should be enabled if the placement count value is any value other than 1. Always value enables clustering regardless of the placement count. Never value disables clustering regardless of the placement count. "
+        i18n-helperText
+      >
+        <option *ngFor="let data of allClustering"
+                i18n>{{ data | upperFirst }}</option>
+      </cds-select>
+    </div>
+
+    <!-- Public addrs -->
+    <ng-container formArrayName="public_addrs"
+                  *ngFor="let _ of public_addrs.controls; index as i">
+      <ng-container [formGroupName]="i">
+        <div cdsRow
+             class="form-item form-item-append">
+          <!-- Address -->
+          <div cdsCol
+               [columnNumbers]="{ lg: 7 }">
+            <cds-text-label
+              for="public_addrs"
+              i18n
+              helperText="This address will be assigned to one of the host's network devices and managed automatically."
+              i18n-helperText
+              cdrequiredField
+              [invalid]="smbForm?.controls['public_addrs']?.controls[i].controls.address.invalid && smbForm?.controls['public_addrs']?.controls[i].controls.address.dirty"
+              [invalidText]="addressError"
+            >Address
+              <input
+                cdsText
+                type="text"
+                formControlName="address"
+                placeholder="192.168.4.51/24"
+                [invalid]="smbForm?.controls['public_addrs'].controls[i].controls.address.invalid && smbForm?.controls['public_addrs']?.controls[i].controls.address.dirty"
+              />
+            </cds-text-label>
+            <ng-template #addressError>
+              <span
+                class="invalid-feedback"
+              >
+                <ng-container i18n> This field is required. </ng-container>
+              </span>
+            </ng-template>
+          </div>
+          <!-- Destination -->
+          <div cdsCol
+               [columnNumbers]="{ lg: 7 }">
+            <cds-text-label
+              for="public_addrs"
+              i18n
+              helperText="Defines where the system will assign the managed IPs. Each string value must be a network address."
+              i18n-helperText
+            >Destination
+            <input
+              cdsText
+              type="text"
+              formControlName="destination"
+              placeholder="192.168.4.0/24"/>
+            </cds-text-label>
+          </div>
+          <div
+            cdsCol
+            [columnNumbers]="{ lg: 1 }"
+            class="item-action-btn spacing"
+          >
+            <cds-icon-button
+              kind="danger"
+              size="sm"
+              (click)="removePublicAddrs(i)"
+            >
+              <svg cdsIcon="trash-can"
+                   size="32"
+                   class="cds--btn__icon"></svg>
+            </cds-icon-button>
+          </div>
+        </div>
+      </ng-container>
+    </ng-container>
+    <div
+      *ngIf="(this.smbForm.get('count').value > 1 && this.smbForm.get('clustering').value.toLowerCase() == CLUSTERING.Default) || this.smbForm.get('clustering').value.toLowerCase() == CLUSTERING.Always"
+      class="form-item"
+    >
+      <button cdsButton="tertiary"
+              type="button"
+              (click)="addPublicAddrs()"
+              i18n>
+        Add public address
+        <svg
+          cdsIcon="add"
+          size="32"
+          class="cds--btn__icon"
+          icon></svg>
       </button>
+      <cd-helper i18n>Assign virtual IP addresses that will be managed
+        by the clustering subsystem and may automatically
+        move between nodes running Samba containers.</cd-helper>
     </div>
     <cd-form-button-panel
       (submitActionEvent)="submitAction()"
index 73bc10c46852b17fb178b06804517f050d59b374..1fa45cd8ae1b0d655cf6af447d1d6cd8e3c69fe4 100644 (file)
@@ -8,6 +8,9 @@ import { FormArray, ReactiveFormsModule, Validators } from '@angular/forms';
 import { ToastrModule } from 'ngx-toastr';
 import { ComboBoxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
 import { AUTHMODE } from '../smb.model';
+import { FOO_USERSGROUPS } from '../smb-usersgroups-form/smb-usersgroups-form.component.spec';
+import { of } from 'rxjs';
+import { By } from '@angular/platform-browser';
 
 describe('SmbClusterFormComponent', () => {
   let component: SmbClusterFormComponent;
@@ -88,4 +91,16 @@ describe('SmbClusterFormComponent', () => {
     component.deleteDomainSettingsModal();
     expect(component).toBeTruthy();
   });
+
+  it('should get usersgroups resources on user authmode', () => {
+    component.smbForm.get('auth_mode').setValue(AUTHMODE.User);
+    component.usersGroups$ = of([FOO_USERSGROUPS]);
+    fixture.whenStable().then(() => {
+      const options = fixture.debugElement.queryAll(By.css('select option'));
+
+      expect(options.length).toBe(1);
+      expect(options[0].nativeElement.value).toBe('foo');
+      expect(options[0].nativeElement.textContent).toBe('foo');
+    });
+  });
 });
index 2ab36f571316aec428a74ed7f9a7e60d4c1b7f88..cbbcd4abca1d979b3b8ecde424e87b5f2b4a11d6 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
 import { Router } from '@angular/router';
 import { forkJoin, Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
@@ -12,7 +12,9 @@ import {
   DomainSettings,
   JoinSource,
   CLUSTER_RESOURCE,
-  ClusterRequestModel
+  ClusterRequestModel,
+  SMBUsersGroups,
+  PublicAddress
 } from '../smb.model';
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { Icons } from '~/app/shared/enum/icons.enum';
@@ -31,6 +33,7 @@ import { SmbService } from '~/app/shared/api/smb.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { SmbDomainSettingModalComponent } from '../smb-domain-setting-modal/smb-domain-setting-modal.component';
 import { CephServicePlacement } from '~/app/shared/models/service.interface';
+import { USERSGROUPS_URL } from '../smb-usersgroups-list/smb-usersgroups-list.component';
 
 @Component({
   selector: 'cd-smb-cluster-form',
@@ -43,6 +46,7 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
   hasOrchestrator: boolean;
   orchStatus$: Observable<any>;
   allClustering: string[] = [];
+  CLUSTERING = CLUSTERING;
   selectedLabels: string[] = [];
   selectedHosts: string[] = [];
   action: string;
@@ -50,6 +54,7 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
   icons = Icons;
   domainSettingsObject: DomainSettings;
   modalData$!: Observable<DomainSettings>;
+  usersGroups$: Observable<SMBUsersGroups[]>;
 
   constructor(
     private hostService: HostService,
@@ -59,7 +64,8 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
     private orchService: OrchestratorService,
     private modalService: ModalCdsService,
     private taskWrapperService: TaskWrapperService,
-    private router: Router
+    private router: Router,
+    private cd: ChangeDetectorRef
   ) {
     super();
     this.resource = $localize`Cluster`;
@@ -67,12 +73,12 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
   }
   ngOnInit() {
     this.action = this.actionLabels.CREATE;
+    this.usersGroups$ = this.smbService.listUsersGroups();
     this.smbService.modalData$.subscribe((data: DomainSettings) => {
       this.domainSettingsObject = data;
       this.smbForm.get('domain_settings').setValue(data?.realm);
     });
     this.createForm();
-
     this.hostsAndLabels$ = forkJoin({
       hosts: this.hostService.getAllHosts(),
       labels: this.hostService.getLabels()
@@ -114,7 +120,8 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
       joinSources: new FormArray([]),
       clustering: new UntypedFormControl(
         CLUSTERING.Default.charAt(0).toUpperCase() + CLUSTERING.Default.slice(1)
-      )
+      ),
+      public_addrs: new FormArray([])
     });
 
     this.orchService.status().subscribe((status) => {
@@ -148,7 +155,7 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
       }
       // Domain Setting should be optional if authMode is "Users"
     } else if (authMode === AUTHMODE.User) {
-      const control = new FormControl('', Validators.required);
+      const control = new FormControl(null, Validators.required);
       userGroupSettingsControl.push(control);
       domainSettingsControl.setErrors(null);
       domainSettingsControl.clearValidators();
@@ -241,6 +248,14 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
       requestModel.cluster_resource.custom_dns = rawFormValue.custom_dns;
     }
 
+    if (rawFormValue.public_addrs?.length > 0) {
+      requestModel.cluster_resource.public_addrs = rawFormValue.public_addrs.map(
+        (publicAddress: PublicAddress) => {
+          return publicAddress.destination ? publicAddress : { address: publicAddress.address };
+        }
+      );
+    }
+
     if (rawFormValue.clustering && rawFormValue.clustering.toLowerCase() !== CLUSTERING.Default) {
       requestModel.cluster_resource.clustering = rawFormValue.clustering.toLowerCase();
     }
@@ -290,21 +305,44 @@ export class SmbClusterFormComponent extends CdForm implements OnInit {
     return this.smbForm.get('custom_dns') as FormArray;
   }
 
+  get public_addrs() {
+    return this.smbForm.get('public_addrs') as FormArray;
+  }
+
   addUserGroupSetting() {
-    const control = new FormControl('', Validators.required);
+    const control = new FormControl(null, Validators.required);
     this.joinSources.push(control);
   }
 
+  navigateCreateUsersGroups() {
+    this.router.navigate([`${USERSGROUPS_URL}/${URLVerbs.CREATE}`]);
+  }
+
   addCustomDns() {
     const control = new FormControl('', Validators.required);
     this.custom_dns.push(control);
   }
 
+  addPublicAddrs() {
+    const control = this.formBuilder.group({
+      address: ['', Validators.required],
+      destination: ['']
+    });
+    this.public_addrs.push(control);
+  }
+
   removeUserGroupSetting(index: number) {
     this.joinSources.removeAt(index);
+    this.cd.detectChanges();
   }
 
   removeCustomDNS(index: number) {
     this.custom_dns.removeAt(index);
+    this.cd.detectChanges();
+  }
+
+  removePublicAddrs(index: number) {
+    this.public_addrs.removeAt(index);
+    this.cd.detectChanges();
   }
 }
index 4413d6486a1fafa825c37ef87ed360721be22457..cdbd2a5f9e26e35243f82075a0bcf315561807a5 100644 (file)
@@ -19,7 +19,6 @@ import { Icons } from '~/app/shared/enum/icons.enum';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
 import { SMBCluster } from '../smb.model';
-import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
@@ -42,7 +41,6 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit {
   selection = new CdTableSelection();
   smbClusters$: Observable<SMBCluster[]>;
   subject$ = new BehaviorSubject<SMBCluster[]>([]);
-  modalRef: NgbModalRef;
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -71,7 +69,7 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit {
     ];
     this.tableActions = [
       {
-        name: `${this.actionLabels.CREATE}`,
+        name: `${this.actionLabels.CREATE} cluster`,
         permission: 'create',
         icon: Icons.add,
         routerLink: () => this.urlBuilder.getCreate(),
index 8d23a4919a89bce2a68baddab883ef64afa2584b..108e25a5a646bd9fa62ea48ddb3d539d705120b1 100644 (file)
@@ -15,7 +15,6 @@
         <div class="form-item">
           <cds-text-label
             label="realm"
-            cdRequiredField="Realm Name"
             [invalid]="
               !domainSettingsForm.controls.realm.valid && domainSettingsForm.controls.realm.dirty
             "
@@ -25,7 +24,6 @@
             <input
               cdsText
               type="text"
-              placeholder="Realm name..."
               formControlName="realm"
               autofocus
             />
           </ng-template>
         </div>
 
-        <!-- Join Source -->
-        <ng-container
-          formArrayName="join_sources"
-          *ngFor="let joinSource of join_sources.controls; index as i"
-        >
-          <ng-container [formGroupName]="i">
-            <div cdsRow
-                 class="form-item form-item-append">
-              <div cdsCol
-                   [columnNumbers]="{ lg: 14 }">
-                <input
-                  cdsText
-                  type="text"
-                  placeholder="Id.."
-                  [id]="'ref' + i"
-                  formControlName="ref"
-                  modal-primary-focus
-                  [invalid]="
-                    !domainSettingsForm.controls['join_sources'].controls[i].valid &&
-                    domainSettingsForm.controls['join_sources'].dirty
-                  "
-                  [invalidText]="refError"
-                />
-                <ng-template #refError>
-                  <span
-                    class="invalid-feedback"
-                    *ngIf="domainSettingsForm.showError('join_sources', formDir, 'required')"
-                    i18n
-                    >This field is required.</span
+        <div
+          *ngIf="joinAuths$ | async as joinAuths"
+          class="form-item">
+          <ng-container
+            formArrayName="join_sources"
+            *ngFor="let _ of join_sources.controls; index as i"
+          >
+            <ng-container [formGroupName]="i">
+              <div
+                cdsRow
+                class="form-item form-item-append">
+                <div
+                  cdsCol
+                  [columnNumbers]="{ lg: 14 }"
+                >
+                  <cds-select
+                    label="Active Directory access resources"
+                    i18n-label
+                    formControlName="ref"
+                    [invalid]="domainSettingsForm.controls?.join_sources.controls[i].invalid && domainSettingsForm.controls?.join_sources.controls[i].controls.ref.dirty"
+                    [invalidText]="joinSourceError"
                   >
-                </ng-template>
-              </div>
-              <div cdsCol
-                   *ngIf="i > 0"
-                   [columnNumbers]="{ lg: 1 }">
-                <cds-icon-button kind="danger"
-                                 size="sm"
-                                 (click)="removeJoinSource(i)">
-                  <svg cdsIcon="trash-can"
-                       size="32"
-                       class="cds--btn__icon"></svg>
-                </cds-icon-button>
+                    <option
+                      [value]="null"
+                      i18n
+                    >-- List of AD access resources --
+                    </option>
+                    <option *ngFor="let jA of joinAuths"
+                            [value]="jA.auth_id">{{ jA.auth_id }}</option>
+                  </cds-select>
+                  <ng-template #joinSourceError>
+                    <span
+                      class="invalid-feedback"
+                    >
+                      <ng-container i18n> This field is required. </ng-container>
+                    </span>
+                  </ng-template>
+                </div>
+                <div cdsCol
+                     *ngIf="i > 0"
+                     [columnNumbers]="{ lg: 1 }"
+                     class="item-action-btn spacing">
+                  <cds-icon-button kind="danger"
+                                   size="sm"
+                                   (click)="removeJoinSource(i)">
+                    <svg cdsIcon="trash-can"
+                         size="32"
+                         class="cds--btn__icon"></svg>
+                  </cds-icon-button>
+                </div>
               </div>
-            </div>
+            </ng-container>
           </ng-container>
-        </ng-container>
-        <div class="form-item">
-          <button cdsButton="tertiary"
-                  type="button"
-                  (click)="addJoinSource()"
-                  i18n>
-            Add Join Source
-            <svg cdsIcon="add"
-                 size="32"
-                 class="cds--btn__icon"
-                 icon></svg>
-          </button>
+          <div class="form-item">
+            <button cdsButton="tertiary"
+                    type="button"
+                    (click)="addJoinSource()"
+                    i18n>
+              Add AD access resource
+              <svg cdsIcon="add"
+                   size="32"
+                   class="cds--btn__icon"
+                   icon></svg>
+            </button>
+            <button cdsButton="tertiary"
+                    type="button"
+                    (click)="navigateCreateJoinSource()"
+                    i18n>
+              Create AD access resource
+              <svg
+                cdsIcon="launch"
+                size="16"
+                class="cds--btn__icon"></svg>
+            </button>
+          </div>
         </div>
       </div>
+
+        <!-- Join Source -->
+
       <cd-form-button-panel
         (submitActionEvent)="submit()"
         [form]="domainSettingsForm"
index 7bb5e131d5ae804edf52430d028cca4e7239e5f9..1258cd71565e3547561a52f2e9b54ad2d2302fda 100644 (file)
@@ -9,6 +9,9 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 import { InputModule, ModalModule, SelectModule } from 'carbon-components-angular';
 import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
+import { FOO_JOIN_AUTH } from '../smb-join-auth-form/smb-join-auth-form.component.spec';
+import { of } from 'rxjs';
+import { By } from '@angular/platform-browser';
 
 describe('SmbDomainSettingModalComponent', () => {
   let component: SmbDomainSettingModalComponent;
@@ -51,4 +54,15 @@ describe('SmbDomainSettingModalComponent', () => {
     component.submit();
     expect(component).toBeTruthy();
   });
+
+  it('should list available join sources', () => {
+    component.joinAuths$ = of([FOO_JOIN_AUTH]);
+    fixture.whenStable().then(() => {
+      const options = fixture.debugElement.queryAll(By.css('select option'));
+
+      expect(options.length).toBe(1);
+      expect(options[0].nativeElement.value).toBe('foo');
+      expect(options[0].nativeElement.textContent).toBe('foo');
+    });
+  });
 });
index 7a9cf1033d41d92a38bbb60292e967042f4839f5..24415b9b1d80f0bef73ad6a80f0627da7410b03d 100644 (file)
@@ -1,7 +1,7 @@
 import { ChangeDetectorRef, Component, Inject, OnInit, Optional } from '@angular/core';
 import { FormArray, FormControl, FormGroup, UntypedFormControl, Validators } from '@angular/forms';
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
-import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
 
@@ -9,7 +9,10 @@ import { NotificationService } from '~/app/shared/services/notification.service'
 import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
 import { SmbService } from '~/app/shared/api/smb.service';
 import { CdForm } from '~/app/shared/forms/cd-form';
-import { DomainSettings } from '../smb.model';
+import { DomainSettings, JoinSource, SMBJoinAuth } from '../smb.model';
+import { Observable } from 'rxjs';
+import { Router } from '@angular/router';
+import { JOINAUTH_URL } from '../smb-join-auth-list/smb-join-auth-list.component';
 
 @Component({
   selector: 'cd-smb-domain-setting-modal',
@@ -19,6 +22,7 @@ import { DomainSettings } from '../smb.model';
 export class SmbDomainSettingModalComponent extends CdForm implements OnInit {
   domainSettingsForm: CdFormGroup;
   realmNames: string[];
+  joinAuths$: Observable<SMBJoinAuth[]>;
 
   constructor(
     public activeModal: NgbActiveModal,
@@ -27,6 +31,7 @@ export class SmbDomainSettingModalComponent extends CdForm implements OnInit {
     public notificationService: NotificationService,
     public smbService: SmbService,
     private cd: ChangeDetectorRef,
+    private router: Router,
     @Optional() @Inject('action') public action: string,
     @Optional() @Inject('resource') public resource: string,
     @Optional()
@@ -35,7 +40,7 @@ export class SmbDomainSettingModalComponent extends CdForm implements OnInit {
   ) {
     super();
     this.action = this.actionLabels.UPDATE;
-    this.resource = $localize`Domain Setting`;
+    this.resource = $localize`Active Directory (AD) parameters`;
   }
 
   private createForm() {
@@ -55,25 +60,18 @@ export class SmbDomainSettingModalComponent extends CdForm implements OnInit {
   ngOnInit(): void {
     this.createForm();
     this.loadingReady();
+    this.joinAuths$ = this.smbService.listJoinAuths();
     this.domainSettingsForm.get('realm').setValue(this.domainSettingsObject?.realm);
     const join_sources = this.domainSettingsForm.get('join_sources') as FormArray;
 
     if (this.domainSettingsObject?.join_sources) {
-      this.domainSettingsObject.join_sources.forEach((source: { ref: string }) => {
-        join_sources.push(
-          new FormGroup({
-            ref: new FormControl(source.ref || '', Validators.required)
-          })
-        );
+      this.domainSettingsObject.join_sources.forEach((source: JoinSource) => {
+        join_sources.push(this.newJoinSource(source));
       });
     }
 
     if (!this.domainSettingsObject) {
-      this.join_sources.push(
-        new FormGroup({
-          ref: new FormControl('', Validators.required)
-        })
-      );
+      this.addJoinSource();
     } else {
       this.action = this.actionLabels.EDIT;
     }
@@ -88,15 +86,22 @@ export class SmbDomainSettingModalComponent extends CdForm implements OnInit {
     return this.domainSettingsForm.get('join_sources') as FormArray;
   }
 
+  newJoinSource(joinSource?: JoinSource) {
+    return new FormGroup({
+      ref: new FormControl(joinSource?.ref || null, Validators.required)
+    });
+  }
+
   addJoinSource() {
-    this.join_sources.push(
-      new FormGroup({
-        ref: new FormControl('', Validators.required)
-      })
-    );
+    this.join_sources.push(this.newJoinSource());
     this.cd.detectChanges();
   }
 
+  navigateCreateJoinSource() {
+    this.closeModal();
+    this.router.navigate([`${JOINAUTH_URL}/${URLVerbs.CREATE}`]);
+  }
+
   removeJoinSource(index: number) {
     const join_sources = this.domainSettingsForm.get('join_sources') as FormArray;
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.html
new file mode 100644 (file)
index 0000000..52b4c05
--- /dev/null
@@ -0,0 +1,126 @@
+<div
+  cdsCol
+  [columnNumbers]="{ md: 4 }"
+>
+  <form name="form"
+        #formDir="ngForm"
+        [formGroup]="form"
+        novalidate>
+    <div i18n="form title"
+         class="form-header">
+      {{ action | titlecase }} {{ resource | upperFirst }}
+    </div>
+
+    <!-- Auth Id -->
+    <div class="form-item">
+      <cds-text-label
+        for="authId"
+        i18n
+        helperText="Unique identifier"
+        i18n-helperText
+        [invalid]="form.controls.authId.invalid && form.controls.authId.dirty"
+        [invalidText]="jaError"
+      >Active directory access resource name
+        <input
+          cdsText
+          type="text"
+          id="authId"
+          name="authId"
+          formControlName="authId"
+          [invalid]="form.controls.authId.invalid && form.controls.authId.dirty"
+        />
+      </cds-text-label>
+      <ng-template #jaError>
+        <span
+          class="invalid-feedback"
+          *ngIf="form.showError('authId', form, 'required')"
+          i18n
+          >This field is required.</span
+        >
+      </ng-template>
+    </div>
+
+    <!-- Username -->
+    <div class="form-item">
+      <cds-text-label
+        for="username"
+        i18n
+        [invalid]="form.controls.username.invalid && form.controls.username.dirty"
+        [invalidText]="usernameError"
+      >Username
+      <input
+          cdsText
+          type="text"
+          id="username"
+          name="username"
+          formControlName="username"
+          [invalid]="form.controls.username.invalid && form.controls.username.dirty"
+      />
+      </cds-text-label>
+      <ng-template #usernameError>
+        <span
+            class="invalid-feedback"
+            *ngIf="form.showError('username', form, 'required')"
+            i18n
+        >This field is required.
+        </span>
+      </ng-template>
+    </div>
+
+    <!-- Password -->
+    <div class="form-item">
+      <cds-password-label
+        for="password"
+        i18n
+        [invalid]="form.controls.password.invalid && form.controls.password.dirty"
+        [invalidText]="passwordError"
+      >Password
+        <input
+          cdsPassword
+          type="password"
+          id="password"
+          name="password"
+          formControlName="password"
+          [invalid]="form.controls.password.invalid && form.controls.password.dirty"
+        />
+      </cds-password-label>
+      <ng-template #passwordError>
+        <span
+            class="invalid-feedback"
+            *ngIf="form.showError('password', form, 'required')"
+            i18n
+        >This field is required.
+        </span>
+      </ng-template>
+    </div>
+
+    <!-- linkedToCluster -->
+    <div
+      *ngIf="smbClusters$ | async as clusters"
+      class="form-item"
+    >
+      <cds-select
+        label="Linked to cluster"
+        i18n-label
+        formControlName="linkedToCluster"
+        name="linkedToCluster"
+        cdOptionalField="Linked to cluster"
+        helperText="This resource may only be used with the linked cluster and will automatically be removed when the linked cluster is removed."
+        i18n-helperText
+      >
+        <option
+          [value]="null"
+          i18n
+        >-- List of clusters --
+        </option>
+        <option *ngFor="let cluster of clusters"
+                [value]="cluster.cluster_id">{{ cluster.cluster_id }}</option>
+      </cds-select>
+    </div>
+    <cd-form-button-panel
+        (submitActionEvent)="submit()"
+        [form]="form"
+        [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+        wrappingClass="text-right"></cd-form-button-panel>
+  </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.spec.ts
new file mode 100644 (file)
index 0000000..810a5c9
--- /dev/null
@@ -0,0 +1,84 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmbJoinAuthFormComponent } from './smb-join-auth-form.component';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { provideRouter } from '@angular/router';
+import { ReactiveFormsModule } from '@angular/forms';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { JOIN_AUTH_RESOURCE } from '../smb.model';
+import { of } from 'rxjs';
+
+export const FOO_JOIN_AUTH = {
+  auth_id: 'foo',
+  auth: {
+    username: 'user',
+    password: 'pass'
+  },
+  resource_type: JOIN_AUTH_RESOURCE
+};
+
+describe('SmbJoinAuthFormComponent', () => {
+  let component: SmbJoinAuthFormComponent;
+  let fixture: ComponentFixture<SmbJoinAuthFormComponent>;
+  let createJoinAuth: jasmine.Spy;
+  let getJoinAuth: jasmine.Spy;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [ToastrModule.forRoot(), SharedModule, ReactiveFormsModule],
+      declarations: [SmbJoinAuthFormComponent],
+      providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SmbJoinAuthFormComponent);
+    component = fixture.componentInstance;
+    component.ngOnInit();
+    createJoinAuth = spyOn(TestBed.inject(SmbService), 'createJoinAuth');
+    getJoinAuth = spyOn(TestBed.inject(SmbService), 'getJoinAuth');
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should set form invalid if any required fields are missing', () => {
+    component.form.controls['authId'].setValue('');
+    component.form.controls['username'].setValue('');
+    component.form.controls['password'].setValue('');
+    expect(component.form.valid).not.toBeNull();
+  });
+
+  it('should submit the form', () => {
+    component.form.controls['authId'].setValue('foo');
+    component.form.controls['username'].setValue('user');
+    component.form.controls['password'].setValue('pass');
+    component.form.controls['linkedToCluster'].setValue(undefined);
+
+    component.submit();
+
+    expect(createJoinAuth).toHaveBeenCalledWith(FOO_JOIN_AUTH);
+  });
+
+  describe('when editing', () => {
+    beforeEach(() => {
+      component.editing = true;
+      getJoinAuth.and.returnValue(of(FOO_JOIN_AUTH));
+      component.ngOnInit();
+      fixture.detectChanges();
+    });
+
+    it('should get resource data and set form fields with it', () => {
+      expect(getJoinAuth).toHaveBeenCalled();
+      expect(component.form.value).toEqual({
+        authId: 'foo',
+        username: 'user',
+        password: 'pass',
+        linkedToCluster: undefined
+      });
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-join-auth-form/smb-join-auth-form.component.ts
new file mode 100644 (file)
index 0000000..308ad15
--- /dev/null
@@ -0,0 +1,113 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { JOIN_AUTH_RESOURCE, SMBCluster, SMBJoinAuth } from '../smb.model';
+import { Observable } from 'rxjs';
+import { JOINAUTH_URL } from '../smb-join-auth-list/smb-join-auth-list.component';
+import { Location } from '@angular/common';
+
+@Component({
+  selector: 'cd-smb-join-auth-form',
+  templateUrl: './smb-join-auth-form.component.html',
+  styleUrls: ['./smb-join-auth-form.component.scss']
+})
+export class SmbJoinAuthFormComponent extends CdForm implements OnInit {
+  form: CdFormGroup;
+  action: string;
+  resource: string;
+  editing: boolean;
+  icons = Icons;
+
+  smbClusters$: Observable<SMBCluster[]>;
+
+  constructor(
+    private actionLabels: ActionLabelsI18n,
+    private taskWrapperService: TaskWrapperService,
+    private formBuilder: CdFormBuilder,
+    private smbService: SmbService,
+    private router: Router,
+    private route: ActivatedRoute,
+    private location: Location
+  ) {
+    super();
+    this.editing = this.router.url.startsWith(`${JOINAUTH_URL}/${URLVerbs.EDIT}`);
+    this.resource = $localize`Active directory (AD) access resource`;
+  }
+
+  ngOnInit() {
+    this.action = this.actionLabels.CREATE;
+    this.smbClusters$ = this.smbService.listClusters();
+    this.createForm();
+
+    if (this.editing) {
+      this.action = this.actionLabels.UPDATE;
+      let editingAuthId: string;
+      this.route.params.subscribe((params: { authId: string }) => {
+        editingAuthId = params.authId;
+      });
+
+      this.smbService.getJoinAuth(editingAuthId).subscribe((joinAuth: SMBJoinAuth) => {
+        this.form.get('authId').setValue(joinAuth.auth_id);
+        this.form.get('username').setValue(joinAuth.auth.username);
+        this.form.get('password').setValue(joinAuth.auth.password);
+        this.form.get('linkedToCluster').setValue(joinAuth.linked_to_cluster);
+      });
+    }
+  }
+
+  createForm() {
+    this.form = this.formBuilder.group({
+      authId: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      username: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      password: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      linkedToCluster: new FormControl(null)
+    });
+  }
+
+  submit() {
+    const authId = this.form.getValue('authId');
+    const username = this.form.getValue('username');
+    const password = this.form.getValue('password');
+    const linkedToCluster = this.form.getValue('linkedToCluster');
+    const BASE_URL = 'smb/ad/';
+
+    const joinAuth: SMBJoinAuth = {
+      resource_type: JOIN_AUTH_RESOURCE,
+      auth_id: authId,
+      auth: { username: username, password: password },
+      linked_to_cluster: linkedToCluster
+    };
+
+    const self = this;
+    let taskUrl = `${BASE_URL}${this.editing ? URLVerbs.EDIT : URLVerbs.CREATE}`;
+    this.taskWrapperService
+      .wrapTaskAroundCall({
+        task: new FinishedTask(taskUrl, {
+          authId: authId
+        }),
+        call: this.smbService.createJoinAuth(joinAuth)
+      })
+      .subscribe({
+        error() {
+          self.form.setErrors({ cdSubmitButton: true });
+        },
+        complete: () => {
+          this.location.back();
+        }
+      });
+  }
+}
index f1818e7ae3a94df29aedbd93d18ea1c75938b6b7..56ee93b88da744ce0b612bacc7bdb64e491ef748 100644 (file)
@@ -5,5 +5,13 @@
   selectionType="single"
   [hasDetails]="false"
   (fetchData)="loadJoinAuth()"
+  (updateSelection)="updateSelection($event)"
 >
+  <div class="table-actions">
+    <cd-table-actions
+      [permission]="permission"
+      [selection]="selection"
+      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
 </cd-table>
index f45cda1084fbdb1513a08e1d693520959326bf99..6cc47f165a8593d4883e02bc6dda7190c727f956 100644 (file)
@@ -9,11 +9,22 @@ import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data
 import { Permission } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { SMBJoinAuth } from '../smb.model';
+import { Router } from '@angular/router';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+export const JOINAUTH_URL = '/cephfs/smb/ad';
 
 @Component({
   selector: 'cd-smb-join-auth-list',
   templateUrl: './smb-join-auth-list.component.html',
-  styleUrls: ['./smb-join-auth-list.component.scss']
+  styleUrls: ['./smb-join-auth-list.component.scss'],
+  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(JOINAUTH_URL) }]
 })
 export class SmbJoinAuthListComponent implements OnInit {
   columns: CdTableColumn[];
@@ -23,11 +34,16 @@ export class SmbJoinAuthListComponent implements OnInit {
 
   joinAuth$: Observable<SMBJoinAuth[]>;
   subject$ = new BehaviorSubject<SMBJoinAuth[]>([]);
+  selection: CdTableSelection = new CdTableSelection();
 
   constructor(
+    private router: Router,
+    private urlBuilder: URLBuilderService,
     private authStorageService: AuthStorageService,
     public actionLabels: ActionLabelsI18n,
-    private smbService: SmbService
+    private smbService: SmbService,
+    private modalService: ModalCdsService,
+    private taskWrapper: TaskWrapperService
   ) {
     this.permission = this.authStorageService.getPermissions().smb;
   }
@@ -35,7 +51,7 @@ export class SmbJoinAuthListComponent implements OnInit {
   ngOnInit() {
     this.columns = [
       {
-        name: $localize`ID`,
+        name: $localize`Name`,
         prop: 'auth_id',
         flexGrow: 2
       },
@@ -45,12 +61,35 @@ export class SmbJoinAuthListComponent implements OnInit {
         flexGrow: 2
       },
       {
-        name: $localize`Linked to Cluster`,
+        name: $localize`Linked to cluster`,
         prop: 'linked_to_cluster',
         flexGrow: 2
       }
     ];
 
+    this.tableActions = [
+      {
+        name: `${this.actionLabels.CREATE} AD`,
+        permission: 'create',
+        icon: Icons.add,
+        click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+      },
+      {
+        name: this.actionLabels.EDIT,
+        permission: 'update',
+        icon: Icons.edit,
+        click: () =>
+          this.router.navigate([this.urlBuilder.getEdit(String(this.selection.first().auth_id))])
+      },
+      {
+        name: this.actionLabels.DELETE,
+        permission: 'update',
+        icon: Icons.destroy,
+        click: () => this.openDeleteModal()
+      }
+    ];
+
     this.joinAuth$ = this.subject$.pipe(
       switchMap(() =>
         this.smbService.listJoinAuths().pipe(
@@ -66,4 +105,24 @@ export class SmbJoinAuthListComponent implements OnInit {
   loadJoinAuth() {
     this.subject$.next([]);
   }
+
+  openDeleteModal() {
+    const authId = this.selection.first().auth_id;
+
+    this.modalService.show(DeleteConfirmationModalComponent, {
+      itemDescription: $localize`Active directory access resource`,
+      itemNames: [authId],
+      submitActionObservable: () =>
+        this.taskWrapper.wrapTaskAroundCall({
+          task: new FinishedTask('smb/ad/remove', {
+            authId: authId
+          }),
+          call: this.smbService.deleteJoinAuth(authId)
+        })
+    });
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
 }
index d2b6f3aac066ecfdcb2b74b0a5c0850a5173c771..0eab75f42f2c37832c06eb1c3b435c70118a4870 100644 (file)
@@ -14,7 +14,7 @@
 >
   Active directory access resources
   <cd-help-text>
-    Logical management units for authorization on Active Directory servers
+    Logical management units for authorization on active directory (AD) servers
   </cd-help-text>
 </legend>
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.html
new file mode 100644 (file)
index 0000000..f6b1db0
--- /dev/null
@@ -0,0 +1,238 @@
+<div
+  cdsCol
+  [columnNumbers]="{ md: 4 }"
+>
+  <form name="form"
+        #formDir="ngForm"
+        [formGroup]="form"
+        novalidate>
+    <div
+      i18n="form title"
+      class="form-header"
+    >
+      {{ action | titlecase }} {{ resource | upperFirst }}
+    </div>
+
+    <!-- Users Groups Id -->
+    <div class="form-item">
+      <cds-text-label
+        for="usersGroupsId"
+        i18n
+        helperText="Unique identifier"
+        i18n-helperText
+        [invalid]="form.controls.usersGroupsId.invalid && form.controls.usersGroupsId.dirty"
+        [invalidText]="usersgroupsError"
+      >Users and groups access resource name
+        <input
+          cdsText
+          type="text"
+          id="usersGroupsId"
+          name="usersGroupsId"
+          formControlName="usersGroupsId"
+        />
+      </cds-text-label>
+      <ng-template #usersgroupsError>
+        <span
+          class="invalid-feedback"
+          *ngIf="form.showError('usersGroupsId', form, 'required')"
+          i18n
+          >This field is required.</span
+        >
+      </ng-template>
+    </div>
+
+    <!-- LinkedToCluster -->
+    <div
+      *ngIf="smbClusters$ | async as clusters"
+      class="form-item"
+    >
+      <cds-select
+        label="Linked to cluster"
+        i18n-label
+        formControlName="linkedToCluster"
+        name="linkedToCluster"
+        helperText="This resource may only be used with the linked cluster and will automatically be removed when the linked cluster is removed."
+        i18n-helperText
+        cdOptionalField="Linked to cluster"
+      >
+        <option
+          [value]="null"
+          i18n
+        >-- List of clusters --
+        </option>
+        <option
+          *ngFor="let cluster of clusters"
+          [value]="cluster.cluster_id">{{ cluster.cluster_id }}
+        </option>
+      </cds-select>
+    </div>
+
+    <!-- Users -->
+    <ng-container
+      formArrayName="users"
+      *ngFor="let _ of users.controls; index as i"
+    >
+      <ng-container [formGroupName]="i">
+        <div
+          cdsRow
+          class="form-item form-item-append"
+        >
+          <div
+            cdsCol
+            [columnNumbers]="{ lg: 7 }"
+          >
+            <cds-text-label
+              for="name"
+              i18n
+              [invalid]="form.controls['users'].controls[i].controls.name.invalid &&
+                          form.controls['users'].controls[i].controls.name.dirty"
+              [invalidText]="usersNameError"
+            >Username
+              <input
+                cdsText
+                type="text"
+                formControlName="name"
+                [invalid]="form.controls['users'].controls[i].controls.name.invalid &&
+                          form.controls['users'].controls[i].controls.name.dirty"
+              />
+            </cds-text-label>
+            <ng-template #usersNameError>
+              <span
+                class="invalid-feedback"
+                i18n
+                >This field is required.</span
+              >
+            </ng-template>
+          </div>
+          <div
+            cdsCol
+            [columnNumbers]="{ lg: 7 }"
+          >
+            <cds-password-label
+              for="password"
+              i18n
+              [invalid]="form.controls['users'].controls[i].controls.password.invalid &&
+                          form.controls['users'].controls[i].controls.password.dirty"
+              [invalidText]="usersPasswordError"
+            >Password
+            <input
+              cdsPassword
+              type="password"
+              label="Password"
+              formControlName="password"
+              [invalid]="form.controls['users'].controls[i].controls.password.invalid &&
+                          form.controls['users'].controls[i].controls.password.dirty"
+            >
+          </cds-password-label>
+          <ng-template #usersPasswordError>
+            <span
+              class="invalid-feedback"
+              i18n
+              >This field is required.</span
+            >
+          </ng-template>
+          </div>
+          <div
+            cdsCol
+            [columnNumbers]="{ lg: 1 }"
+            class="item-action-btn spacing"
+          >
+            <cds-icon-button
+              kind="danger"
+              *ngIf="i > 0"
+              size="sm"
+              (click)="removeUser(i)"
+            >
+            <svg
+              cdsIcon="trash-can"
+              size="32"
+              class="cds--btn__icon">
+            </svg>
+          </cds-icon-button>
+        </div>
+        </div>
+      </ng-container>
+    </ng-container>
+
+    <div class="form-item">
+      <button
+        cdsButton="tertiary"
+        type="button"
+        (click)="addUser()"
+        i18n
+      >Add User
+        <svg
+          cdsIcon="add"
+          size="32"
+          class="cds--btn__icon"
+          icon>
+        </svg>
+      </button>
+    </div>
+
+    <!-- Groups -->
+    <ng-container
+      formArrayName="groups"
+      *ngFor="let _ of groups.controls; index as i"
+    >
+      <ng-container [formGroupName]="i">
+        <div
+          cdsRow
+          class="form-item form-item-append"
+        >
+          <div
+            cdsCol
+            [columnNumbers]="{ lg: 14 }"
+          >
+          <cds-text-label
+            for="name"
+            i18n
+          >Group
+            <input
+              cdsText
+              type="text"
+              formControlName="name"
+            />
+          </cds-text-label>
+          </div>
+          <div
+            cdsCol
+            [columnNumbers]="{ lg: 1 }"
+            class="item-action-btn spacing">
+            <cds-icon-button
+              kind="danger"
+              size="sm"
+              (click)="removeGroup(i)"
+            >
+            <svg
+              cdsIcon="trash-can"
+              size="32"
+              class="cds--btn__icon">
+            </svg>
+          </cds-icon-button>
+          </div>
+        </div>
+      </ng-container>
+    </ng-container>
+
+    <div class="form-item">
+      <button cdsButton="tertiary"
+              type="button"
+              (click)="addGroup()"
+              i18n>
+        Add Group
+        <svg
+          cdsIcon="add"
+          size="32"
+          class="cds--btn__icon"
+          icon></svg>
+      </button>
+    </div>
+
+    <cd-form-button-panel
+      (submitActionEvent)="submit()"
+      [form]="form"
+      [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+      wrappingClass="text-right"></cd-form-button-panel>
+    </form>
+  </div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.scss
new file mode 100644 (file)
index 0000000..7aa2236
--- /dev/null
@@ -0,0 +1,5 @@
+@use '@carbon/layout';
+
+.spacing {
+  margin-top: layout.$spacing-06;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.spec.ts
new file mode 100644 (file)
index 0000000..19752c4
--- /dev/null
@@ -0,0 +1,120 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmbUsersgroupsFormComponent } from './smb-usersgroups-form.component';
+import { ToastrModule } from 'ngx-toastr';
+import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { provideRouter } from '@angular/router';
+import { SharedModule } from '~/app/shared/shared.module';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { USERSGROUPS_RESOURCE } from '../smb.model';
+import { of } from 'rxjs';
+
+export const FOO_USERSGROUPS = {
+  users_groups_id: 'foo',
+  values: {
+    users: [
+      {
+        name: 'user',
+        password: 'pass'
+      }
+    ],
+    groups: [
+      {
+        name: 'bar'
+      }
+    ]
+  },
+  resource_type: USERSGROUPS_RESOURCE
+};
+
+describe('SmbUsersgroupsFormComponent', () => {
+  let component: SmbUsersgroupsFormComponent;
+  let fixture: ComponentFixture<SmbUsersgroupsFormComponent>;
+  let createUsersGroups: jasmine.Spy;
+  let getUsersGroups: jasmine.Spy;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [ToastrModule.forRoot(), SharedModule, ReactiveFormsModule],
+      declarations: [SmbUsersgroupsFormComponent],
+      providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SmbUsersgroupsFormComponent);
+    component = fixture.componentInstance;
+    component.ngOnInit();
+    createUsersGroups = spyOn(TestBed.inject(SmbService), 'createUsersGroups');
+    getUsersGroups = spyOn(TestBed.inject(SmbService), 'getUsersGroups');
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should set form invalid if required username is missing', () => {
+    const user = component.users.controls[0] as FormGroup;
+    component.form.controls['usersGroupsId'].setValue('foo');
+    user.controls['name'].setValue('');
+    expect(component.form.valid).not.toBeNull();
+  });
+
+  it('should set required fields, add group and submit the form', () => {
+    const user = component.users.controls[0] as FormGroup;
+    component.form.controls['usersGroupsId'].setValue('foo');
+    component.form.controls['linkedToCluster'].setValue(undefined);
+    user.controls['name'].setValue('user');
+    user.controls['password'].setValue('pass');
+    component.addGroup();
+    const group = component.groups.controls[0] as FormGroup;
+    group.controls['name'].setValue('bar');
+
+    component.submit();
+
+    expect(createUsersGroups).toHaveBeenCalledWith(FOO_USERSGROUPS);
+  });
+
+  describe('when editing', () => {
+    beforeEach(() => {
+      component.editing = true;
+      getUsersGroups.and.returnValue(of(FOO_USERSGROUPS));
+      component.ngOnInit();
+      fixture.detectChanges();
+    });
+
+    it('should get resource data and set form fields with it', () => {
+      expect(getUsersGroups).toHaveBeenCalled();
+      expect(component.form.value).toEqual({
+        usersGroupsId: 'foo',
+        users: [
+          {
+            name: 'user',
+            password: 'pass'
+          }
+        ],
+        groups: [
+          {
+            name: 'bar'
+          }
+        ],
+        linkedToCluster: undefined
+      });
+    });
+  });
+
+  it('should add and remove users and groups', () => {
+    const nUsers = component.users.length;
+    const nGroups = component.groups.length;
+    component.addUser();
+    component.addGroup();
+    component.addGroup();
+    expect(component.users.length).toBe(nUsers + 1);
+    expect(component.groups.length).toBe(nGroups + 2);
+    component.removeUser(0);
+    component.removeGroup(0);
+    expect(component.users.length).toBe(nUsers);
+    expect(component.groups.length).toBe(nGroups + 1);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component.ts
new file mode 100644 (file)
index 0000000..8f8fa68
--- /dev/null
@@ -0,0 +1,158 @@
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { FormArray, FormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Observable } from 'rxjs';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { Group, SMBCluster, SMBUsersGroups, User, USERSGROUPS_RESOURCE } from '../smb.model';
+import { Location } from '@angular/common';
+import { USERSGROUPS_URL } from '../smb-usersgroups-list/smb-usersgroups-list.component';
+
+@Component({
+  selector: 'cd-smb-usersgroups-form',
+  templateUrl: './smb-usersgroups-form.component.html',
+  styleUrls: ['./smb-usersgroups-form.component.scss']
+})
+export class SmbUsersgroupsFormComponent extends CdForm implements OnInit {
+  form: CdFormGroup;
+  action: string;
+  resource: string;
+  editing: boolean;
+  icons = Icons;
+
+  smbClusters$: Observable<SMBCluster[]>;
+
+  constructor(
+    private actionLabels: ActionLabelsI18n,
+    private taskWrapperService: TaskWrapperService,
+    private formBuilder: CdFormBuilder,
+    private smbService: SmbService,
+    private router: Router,
+    private cd: ChangeDetectorRef,
+    private route: ActivatedRoute,
+    private location: Location
+  ) {
+    super();
+    this.editing = this.router.url.startsWith(`${USERSGROUPS_URL}/${URLVerbs.EDIT}`);
+    this.resource = $localize`users and groups access resource`;
+  }
+
+  ngOnInit() {
+    this.action = this.actionLabels.CREATE;
+    this.smbClusters$ = this.smbService.listClusters();
+    this.createForm();
+
+    if (this.editing) {
+      this.action = this.actionLabels.UPDATE;
+      let editingUsersGroupId: string;
+      this.route.params.subscribe((params: { usersGroupsId: string }) => {
+        editingUsersGroupId = params.usersGroupsId;
+      });
+      this.smbService
+        .getUsersGroups(editingUsersGroupId)
+        .subscribe((usersGroups: SMBUsersGroups) => {
+          this.form.get('usersGroupsId').setValue(usersGroups.users_groups_id);
+          this.form.get('linkedToCluster').setValue(usersGroups.linked_to_cluster);
+
+          usersGroups.values.users.forEach((user: User) => {
+            this.addUser(user);
+          });
+
+          usersGroups.values.groups.forEach((group: Group) => {
+            this.addGroup(group);
+          });
+        });
+    } else {
+      this.addUser();
+    }
+  }
+
+  createForm() {
+    this.form = this.formBuilder.group({
+      usersGroupsId: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      linkedToCluster: new FormControl(null),
+      users: new FormArray([]),
+      groups: new FormArray([])
+    });
+  }
+
+  submit() {
+    const usersGroupsId = this.form.getValue('usersGroupsId');
+    const linkedToCluster = this.form.getValue('linkedToCluster');
+    const users = this.form.getValue('users');
+    const groups = this.form.getValue('groups');
+    const usersgroups: SMBUsersGroups = {
+      resource_type: USERSGROUPS_RESOURCE,
+      users_groups_id: usersGroupsId,
+      values: { users: users, groups: groups },
+      linked_to_cluster: linkedToCluster
+    };
+
+    const self = this;
+    const BASE_URL = 'smb/standalone/';
+
+    let taskUrl = `${BASE_URL}${this.editing ? URLVerbs.EDIT : URLVerbs.CREATE}`;
+    this.taskWrapperService
+      .wrapTaskAroundCall({
+        task: new FinishedTask(taskUrl, {
+          usersGroupsId: usersGroupsId
+        }),
+        call: this.smbService.createUsersGroups(usersgroups)
+      })
+      .subscribe({
+        error() {
+          self.form.setErrors({ cdSubmitButton: true });
+        },
+        complete: () => {
+          this.location.back();
+        }
+      });
+  }
+
+  get users(): FormArray {
+    return this.form.get('users') as FormArray;
+  }
+
+  get groups(): FormArray {
+    return this.form.get('groups') as FormArray;
+  }
+
+  newUser(user?: User): CdFormGroup {
+    return this.formBuilder.group({
+      name: [user ? user.name : '', Validators.required],
+      password: [user ? user.password : '', [Validators.required]]
+    });
+  }
+
+  newGroup(group?: Group): CdFormGroup {
+    return this.formBuilder.group({
+      name: [group ? group.name : '']
+    });
+  }
+
+  addUser(user?: User): void {
+    this.users.push(this.newUser(user));
+  }
+
+  addGroup(group?: Group): void {
+    this.groups.push(this.newGroup(group));
+  }
+
+  removeUser(index: number): void {
+    this.users.removeAt(index);
+    this.cd.detectChanges();
+  }
+
+  removeGroup(index: number): void {
+    this.groups.removeAt(index);
+    this.cd.detectChanges();
+  }
+}
index 7aa7ee37d31f25e788cd52243e8afc90c316261e..b0119dbe4392ae724bfd82012736506c4559a2ff 100644 (file)
@@ -7,7 +7,15 @@
     [hasDetails]="true"
     (setExpandedRow)="setExpandedRow($event)"
     (fetchData)="loadUsersGroups()"
->
+    (updateSelection)="updateSelection($event)"
+  >
+  <div class="table-actions">
+    <cd-table-actions
+      [permission]="permission"
+      [selection]="selection"
+      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
   <cd-smb-usersgroups-details
     *cdTableDetail
     [selection]="expandedRow"
@@ -23,6 +31,6 @@
     *ngFor="let group of row?.values.groups"
     size="md"
   >
-    {{ group.name }}
+    {{ group?.name }}
   </cds-tag>
 </ng-template>
index 869a21115da870ecf3d17fb85933e42e2afb90a4..2d52f53e990b1a017d2c49769ee9f2f34da35578 100644 (file)
@@ -14,11 +14,22 @@ import { Permission } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { SmbService } from '~/app/shared/api/smb.service';
 import { SMBUsersGroups } from '../smb.model';
+import { Router } from '@angular/router';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+export const USERSGROUPS_URL = '/cephfs/smb/standalone';
 
 @Component({
   selector: 'cd-smb-users-list',
   templateUrl: './smb-usersgroups-list.component.html',
-  styleUrls: ['./smb-usersgroups-list.component.scss']
+  styleUrls: ['./smb-usersgroups-list.component.scss'],
+  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(USERSGROUPS_URL) }]
 })
 export class SmbUsersgroupsListComponent extends ListWithDetails implements OnInit {
   @ViewChild('groupsNamesTpl', { static: true })
@@ -30,11 +41,16 @@ export class SmbUsersgroupsListComponent extends ListWithDetails implements OnIn
 
   usersGroups$: Observable<SMBUsersGroups[]>;
   subject$ = new BehaviorSubject<SMBUsersGroups[]>([]);
+  selection: CdTableSelection = new CdTableSelection();
 
   constructor(
+    private router: Router,
+    private urlBuilder: URLBuilderService,
     private authStorageService: AuthStorageService,
     public actionLabels: ActionLabelsI18n,
-    private smbService: SmbService
+    private smbService: SmbService,
+    private modalService: ModalCdsService,
+    private taskWrapper: TaskWrapperService
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().smb;
@@ -43,7 +59,7 @@ export class SmbUsersgroupsListComponent extends ListWithDetails implements OnIn
   ngOnInit() {
     this.columns = [
       {
-        name: $localize`ID`,
+        name: $localize`Name`,
         prop: 'users_groups_id',
         flexGrow: 2
       },
@@ -59,12 +75,37 @@ export class SmbUsersgroupsListComponent extends ListWithDetails implements OnIn
         flexGrow: 2
       },
       {
-        name: $localize`Linked to`,
+        name: $localize`Linked to cluster`,
         prop: 'values.linked_to_cluster',
         flexGrow: 2
       }
     ];
 
+    this.tableActions = [
+      {
+        name: `${this.actionLabels.CREATE} standalone`,
+        permission: 'create',
+        icon: Icons.add,
+        click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+      },
+      {
+        name: this.actionLabels.EDIT,
+        permission: 'update',
+        icon: Icons.edit,
+        click: () =>
+          this.router.navigate([
+            this.urlBuilder.getEdit(String(this.selection.first().users_groups_id))
+          ])
+      },
+      {
+        name: this.actionLabels.DELETE,
+        permission: 'delete',
+        icon: Icons.destroy,
+        click: () => this.openDeleteModal()
+      }
+    ];
+
     this.usersGroups$ = this.subject$.pipe(
       switchMap(() =>
         this.smbService.listUsersGroups().pipe(
@@ -80,4 +121,24 @@ export class SmbUsersgroupsListComponent extends ListWithDetails implements OnIn
   loadUsersGroups() {
     this.subject$.next([]);
   }
+
+  openDeleteModal() {
+    const usersGroupsId = this.selection.first().users_groups_id;
+
+    this.modalService.show(DeleteConfirmationModalComponent, {
+      itemDescription: $localize`Users and groups access resource`,
+      itemNames: [usersGroupsId],
+      submitActionObservable: () =>
+        this.taskWrapper.wrapTaskAroundCall({
+          task: new FinishedTask('smb/standalone/remove', {
+            usersGroupsId: usersGroupsId
+          }),
+          call: this.smbService.deleteUsersgroups(usersGroupsId)
+        })
+    });
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
 }
index 1d4aff01353a7850cf1c5aecf68938150a9f0ee0..8f1ea56bbd504d061fee84cb5b5cf36808f6d5c3 100644 (file)
@@ -1,7 +1,7 @@
 import { CephServicePlacement } from '~/app/shared/models/service.interface';
 
 export interface SMBCluster {
-  resource_type: string;
+  resource_type: typeof CLUSTER_RESOURCE;
   cluster_id: string;
   auth_mode: typeof AUTHMODE[keyof typeof AUTHMODE];
   domain_settings?: DomainSettings;
@@ -9,7 +9,7 @@ export interface SMBCluster {
   custom_dns?: string[];
   placement?: CephServicePlacement;
   clustering?: typeof CLUSTERING;
-  public_addrs?: PublicAddress;
+  public_addrs?: PublicAddress[];
 }
 
 export interface ClusterRequestModel {
@@ -43,15 +43,17 @@ export interface DomainSettings {
   realm?: string;
   join_sources?: JoinSource[];
 }
-export interface PublicAddress {
-  address: string;
-  destination: string;
-}
+
 export interface JoinSource {
-  source_type: string;
+  source_type?: string;
   ref: string;
 }
 
+export interface PublicAddress {
+  address: string;
+  destination?: string;
+}
+
 export const CLUSTERING = {
   Default: 'default',
   Always: 'always',
@@ -103,7 +105,6 @@ interface SMBShareLoginControl {
 export interface SMBJoinAuth {
   resource_type: string;
   auth_id: string;
-  intent: Intent;
   auth: Auth;
   linked_to_cluster?: string;
 }
@@ -111,7 +112,6 @@ export interface SMBJoinAuth {
 export interface SMBUsersGroups {
   resource_type: string;
   users_groups_id: string;
-  intent: Intent;
   values: Value;
   linked_to_cluster?: string;
 }
@@ -121,12 +121,12 @@ interface Auth {
   password: string;
 }
 
-interface User {
+export interface User {
   name: string;
   password: string;
 }
 
-interface Group {
+export interface Group {
   name: string;
 }
 
@@ -135,10 +135,9 @@ interface Value {
   groups: Group[];
 }
 
-type Intent = 'present' | 'removed';
-
-export const CLUSTER_RESOURCE = 'ceph.smb.cluster';
-
-export const SHARE_RESOURCE = 'ceph.smb.share';
+export const CLUSTER_RESOURCE = 'ceph.smb.cluster' as const;
+export const SHARE_RESOURCE = 'ceph.smb.share' as const;
+export const JOIN_AUTH_RESOURCE = 'ceph.smb.join.auth' as const;
+export const USERSGROUPS_RESOURCE = 'ceph.smb.usersgroups' as const;
 
 export const PROVIDER = 'samba-vfs';
index a375da67e743ba9d20372cc61690975b698a6170..d2ff1c9727e063765b33c2d828e65867058f640e 100644 (file)
@@ -21,7 +21,8 @@ import {
   PlaceholderModule,
   SelectModule,
   TabsModule,
-  TagModule
+  TagModule,
+  FileUploaderModule
 } from 'carbon-components-angular';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { CommonModule } from '@angular/common';
@@ -34,11 +35,11 @@ import { SmbUsersgroupsListComponent } from './smb-usersgroups-list/smb-usersgro
 import { SmbTabsComponent } from './smb-tabs/smb-tabs.component';
 import { SmbJoinAuthListComponent } from './smb-join-auth-list/smb-join-auth-list.component';
 import { SmbUsersgroupsDetailsComponent } from './smb-usersgroups-details/smb-usersgroups-details.component';
-import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
+import { SmbJoinAuthFormComponent } from './smb-join-auth-form/smb-join-auth-form.component';
+import { SmbUsersgroupsFormComponent } from './smb-usersgroups-form/smb-usersgroups-form.component';
 
 @NgModule({
   imports: [
-    ReactiveFormsModule,
     RouterModule,
     CommonModule,
     SharedModule,
@@ -51,6 +52,7 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
     SelectModule,
     TabsModule,
     TagModule,
+    FileUploaderModule,
     InputModule,
     CheckboxModule,
     SelectModule,
@@ -74,9 +76,11 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
     SmbUsersgroupsDetailsComponent,
     SmbTabsComponent,
     SmbJoinAuthListComponent,
+    SmbUsersgroupsDetailsComponent,
+    SmbJoinAuthFormComponent,
+    SmbUsersgroupsFormComponent,
     SmbShareFormComponent
-  ],
-  providers: [provideCharts(withDefaultRegisterables())]
+  ]
 })
 export class SmbModule {
   constructor(private iconService: IconService) {
index 34f1156a72fb5015f38f3721dc9cbfaf9cef461e..a263a11b94d0dadb9f2f7b8c55b8c554f25dbdc9 100644 (file)
@@ -4,6 +4,11 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common
 import { SmbService } from './smb.service';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { provideHttpClient } from '@angular/common/http';
+import {
+  CLUSTER_RESOURCE,
+  JOIN_AUTH_RESOURCE,
+  USERSGROUPS_RESOURCE
+} from '~/app/ceph/smb/smb.model';
 
 describe('SmbService', () => {
   let service: SmbService;
@@ -32,10 +37,9 @@ describe('SmbService', () => {
   it('should call create cluster', () => {
     const request = {
       cluster_resource: {
-        resource_type: 'ceph.smb.cluster',
+        resource_type: CLUSTER_RESOURCE,
         cluster_id: 'clusterUserTest',
         auth_mode: 'active-directory',
-        intent: 'present',
         domain_settings: {
           realm: 'DOMAIN1.SINK.TEST',
           join_sources: [
@@ -74,12 +78,51 @@ describe('SmbService', () => {
     expect(req.request.method).toBe('GET');
   });
 
+  it('should call create join auth', () => {
+    const request = {
+      resource_type: JOIN_AUTH_RESOURCE,
+      auth_id: 'foo',
+      auth: {
+        username: 'user',
+        password: 'pass'
+      },
+      linked_to_cluster: ''
+    };
+    service.createJoinAuth(request).subscribe();
+    const req = httpTesting.expectOne('api/smb/joinauth');
+    expect(req.request.method).toBe('POST');
+  });
+
   it('should call list usersgroups', () => {
     service.listUsersGroups().subscribe();
     const req = httpTesting.expectOne('api/smb/usersgroups');
     expect(req.request.method).toBe('GET');
   });
 
+  it('should call create usersgroups', () => {
+    const request = {
+      resource_type: USERSGROUPS_RESOURCE,
+      users_groups_id: 'foo',
+      values: {
+        users: [
+          {
+            name: 'user',
+            password: 'pass'
+          }
+        ],
+        groups: [
+          {
+            name: 'bar'
+          }
+        ]
+      },
+      linked_to_cluster: ''
+    };
+    service.createUsersGroups(request).subscribe();
+    const req = httpTesting.expectOne('api/smb/usersgroups');
+    expect(req.request.method).toBe('POST');
+  });
+
   it('should call create share', () => {
     const request = {
       share_resource: {
index c719519c12d74766ddd5dfa03963471d52337655..d51196ced9cb9ed7d5969a94732f77d44879c8cd 100644 (file)
@@ -61,4 +61,36 @@ export class SmbService {
       observe: 'response'
     });
   }
+
+  getJoinAuth(authId: string): Observable<SMBJoinAuth> {
+    return this.http.get<SMBJoinAuth>(`${this.baseURL}/joinauth/${authId}`);
+  }
+
+  getUsersGroups(usersGroupsId: string): Observable<SMBUsersGroups> {
+    return this.http.get<SMBUsersGroups>(`${this.baseURL}/usersgroups/${usersGroupsId}`);
+  }
+
+  createJoinAuth(joinAuth: SMBJoinAuth) {
+    return this.http.post(`${this.baseURL}/joinauth`, {
+      join_auth: joinAuth
+    });
+  }
+
+  createUsersGroups(usersgroups: SMBUsersGroups) {
+    return this.http.post(`${this.baseURL}/usersgroups`, {
+      usersgroups: usersgroups
+    });
+  }
+
+  deleteJoinAuth(authId: string) {
+    return this.http.delete(`${this.baseURL}/joinauth/${authId}`, {
+      observe: 'response'
+    });
+  }
+
+  deleteUsersgroups(usersGroupsId: string) {
+    return this.http.delete(`${this.baseURL}/usersgroups/${usersGroupsId}`, {
+      observe: 'response'
+    });
+  }
 }
index a943abe0febe4a7e7f29f46991225d80768c0b17..281b3f714c981e2dd88894ba9e938c30fd54e798 100644 (file)
@@ -408,6 +408,24 @@ export class TaskMessageService {
     'smb/cluster/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
       this.smbCluster(metadata)
     ),
+    'smb/ad/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.smbJoinAuth(metadata)
+    ),
+    'smb/ad/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+      this.smbJoinAuth(metadata)
+    ),
+    'smb/ad/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+      this.smbJoinAuth(metadata)
+    ),
+    'smb/standalone/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.smbUsersgroups(metadata)
+    ),
+    'smb/standalone/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+      this.smbUsersgroups(metadata)
+    ),
+    'smb/standalone/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+      this.smbUsersgroups(metadata)
+    ),
     // Grafana tasks
     'grafana/dashboards/update': this.newTaskMessage(
       this.commonOperations.update,
@@ -501,6 +519,12 @@ export class TaskMessageService {
     ),
     'smb/share/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
       this.smbShare(metadata)
+    ),
+    'cephfs/smb/joinauth/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.smbJoinAuth(metadata)
+    ),
+    'cephfs/smb/standalone/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.smbUsersgroups(metadata)
     )
   };
 
@@ -568,6 +592,14 @@ export class TaskMessageService {
     return $localize`SMB Cluster  '${metadata.cluster_id}'`;
   }
 
+  smbJoinAuth(metadata: { authId: string }) {
+    return $localize`SMB active directory access resource '${metadata.authId}'`;
+  }
+
+  smbUsersgroups(metadata: { usersGroupsId: string }) {
+    return $localize`SMB users and groups access resource '${metadata.usersGroupsId}'`;
+  }
+
   service(metadata: any) {
     return $localize`service '${metadata.service_name}'`;
   }
index dc2ccc370553d9b9ae75d610df10470967a97030..52e6e4e0bdc85d3000a752ca9ba5ddb27c01f493 100644 (file)
@@ -14893,11 +14893,11 @@ paths:
               schema:
                 properties:
                   results:
-                    description: List of results with resource details
+                    description: List of operation results
                     items:
                       properties:
                         resource:
-                          description: Resource details
+                          description: Resource
                           properties:
                             auth_mode:
                               description: Either 'active-directory' or 'user'
@@ -14906,7 +14906,7 @@ paths:
                               description: Unique identifier for the cluster
                               type: string
                             custom_dns:
-                              description: List of custom DNS server addresses (optional)
+                              description: List of custom DNS server addresses
                               items:
                                 type: string
                               type: array
@@ -14944,7 +14944,6 @@ paths:
                               type: string
                             placement:
                               description: Placement configuration for the resource
-                                (optional)
                               properties:
                                 count:
                                   description: Number of instances to place
@@ -14957,7 +14956,6 @@ paths:
                               type: string
                             user_group_settings:
                               description: User group settings for user auth mode
-                                (optional)
                               items:
                                 properties:
                                   ref:
@@ -14983,10 +14981,11 @@ paths:
                           - placement
                           type: object
                         state:
-                          description: State of the resource
+                          description: The current state of the resource,                        e.g.,
+                            'created', 'updated', 'deleted'
                           type: string
                         success:
-                          description: Indicates whether the operation was successful
+                          description: Indicates if the operation was successful
                           type: boolean
                       required:
                       - resource
@@ -14995,7 +14994,7 @@ paths:
                       type: object
                     type: array
                   success:
-                    description: Overall success status of the operation
+                    description: Indicates if the overall operation was successful
                     type: boolean
                 required:
                 - results
@@ -15172,12 +15171,7 @@ paths:
     get:
       description: "\n        List all smb join auth resources\n\n        :return:\
         \ Returns list of join auth.\n        :rtype: List[Dict]\n        "
-      parameters:
-      - default: ''
-        in: query
-        name: join_auth
-        schema:
-          type: string
+      parameters: []
       responses:
         '200':
           content:
@@ -15236,6 +15230,211 @@ paths:
       summary: List smb join authorization resources
       tags:
       - SMB
+    post:
+      description: "\n        Create smb join auth resource\n\n        :return: Returns\
+        \ join auth resource.\n        :rtype: Dict\n        "
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                join_auth:
+                  type: string
+              required:
+              - join_auth
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                properties:
+                  results:
+                    description: List of operation results
+                    items:
+                      properties:
+                        resource:
+                          description: Resource
+                          properties:
+                            auth:
+                              description: Authentication credentials
+                              properties:
+                                password:
+                                  description: Password for authentication
+                                  type: string
+                                username:
+                                  description: Username for authentication
+                                  type: string
+                              required:
+                              - username
+                              - password
+                              type: object
+                            auth_id:
+                              description: Unique identifier for the join auth resource
+                              type: string
+                            intent:
+                              description: Desired state of the resource, e.g., 'present'
+                                or 'removed'
+                              type: string
+                            linked_to_cluster:
+                              description: Optional string containing a cluster ID.     If
+                                set, the resource is linked to the cluster and will
+                                be automatically removed     when the cluster is removed
+                              type: string
+                            resource_type:
+                              description: ceph.smb.join.auth
+                              type: string
+                          required:
+                          - resource_type
+                          - auth_id
+                          - intent
+                          - auth
+                          - linked_to_cluster
+                          type: object
+                        state:
+                          description: The current state of the resource,                        e.g.,
+                            'created', 'updated', 'deleted'
+                          type: string
+                        success:
+                          description: Indicates if the operation was successful
+                          type: boolean
+                      required:
+                      - resource
+                      - state
+                      - success
+                      type: object
+                    type: array
+                  success:
+                    description: Indicates if the overall operation was successful
+                    type: boolean
+                required:
+                - results
+                - success
+                type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Create smb join auth
+      tags:
+      - SMB
+  /api/smb/joinauth/{auth_id}:
+    delete:
+      description: "\n        Delete smb join auth resource\n\n        :param auth_id:\
+        \ Join Auth identifier\n        :return: None.\n        "
+      parameters:
+      - description: auth_id
+        in: path
+        name: auth_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                properties: {}
+                type: object
+          description: Resource deleted.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Delete smb join auth
+      tags:
+      - SMB
+    get:
+      description: "\n        Get Join auth resource\n\n        :return: Returns join\
+        \ auth.\n        :rtype: Dict\n        "
+      parameters:
+      - in: path
+        name: auth_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                properties:
+                  auth:
+                    description: Authentication credentials
+                    properties:
+                      password:
+                        description: Password for authentication
+                        type: string
+                      username:
+                        description: Username for authentication
+                        type: string
+                    required:
+                    - username
+                    - password
+                    type: object
+                  auth_id:
+                    description: Unique identifier for the join auth resource
+                    type: string
+                  intent:
+                    description: Desired state of the resource, e.g., 'present' or
+                      'removed'
+                    type: string
+                  linked_to_cluster:
+                    description: Optional string containing a cluster ID.     If set,
+                      the resource is linked to the cluster and will be automatically
+                      removed     when the cluster is removed
+                    type: string
+                  resource_type:
+                    description: ceph.smb.join.auth
+                    type: string
+                required:
+                - resource_type
+                - auth_id
+                - intent
+                - auth
+                - linked_to_cluster
+                type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Get smb join authorization resource
+      tags:
+      - SMB
   /api/smb/share:
     get:
       description: "\n        List all smb shares or all shares for a given cluster\n\
@@ -15349,11 +15548,11 @@ paths:
               schema:
                 properties:
                   results:
-                    description: List of results with resource details
+                    description: List of operation results
                     items:
                       properties:
                         resource:
-                          description: Resource details
+                          description: Resource
                           properties:
                             browseable:
                               description: Indicates if the share is browseable
@@ -15380,9 +15579,9 @@ paths:
                               required:
                               - volume
                               - path
+                              - provider
                               - subvolumegroup
                               - subvolume
-                              - provider
                               type: object
                             cluster_id:
                               description: Unique identifier for the cluster
@@ -15414,10 +15613,11 @@ paths:
                           - cephfs
                           type: object
                         state:
-                          description: State of the resource
+                          description: The current state of the resource,                        e.g.,
+                            'created', 'updated', 'deleted'
                           type: string
                         success:
-                          description: Indicates whether the operation was successful
+                          description: Indicates if the operation was successful
                           type: boolean
                       required:
                       - resource
@@ -15426,7 +15626,7 @@ paths:
                       type: object
                     type: array
                   success:
-                    description: Overall success status of the operation
+                    description: Indicates if the overall operation was successful
                     type: boolean
                 required:
                 - results
@@ -15500,13 +15700,8 @@ paths:
   /api/smb/usersgroups:
     get:
       description: "\n        List all smb usersgroups resources\n\n        :return:\
-        \ Returns list of usersgroups.\n        :rtype: List[Dict]\n        "
-      parameters:
-      - default: ''
-        in: query
-        name: users_groups
-        schema:
-          type: string
+        \ Returns list of usersgroups\n        :rtype: List[Dict]\n        "
+      parameters: []
       responses:
         '200':
           content:
@@ -15586,6 +15781,262 @@ paths:
       summary: List smb user resources
       tags:
       - SMB
+    post:
+      description: "\n        Create smb usersgroups resource\n\n        :return:\
+        \ Returns usersgroups resource.\n        :rtype: Dict\n        "
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                usersgroups:
+                  type: string
+              required:
+              - usersgroups
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                properties:
+                  results:
+                    description: List of operation results
+                    items:
+                      properties:
+                        resource:
+                          description: Resource
+                          properties:
+                            results:
+                              description: List of operation results
+                              items:
+                                properties:
+                                  resource:
+                                    description: Resource
+                                    properties:
+                                      auth:
+                                        description: Authentication credentials
+                                        properties:
+                                          password:
+                                            description: Password for authentication
+                                            type: string
+                                          username:
+                                            description: Username for authentication
+                                            type: string
+                                        required:
+                                        - username
+                                        - password
+                                        type: object
+                                      auth_id:
+                                        description: Unique identifier for the join
+                                          auth resource
+                                        type: string
+                                      intent:
+                                        description: Desired state of the resource,
+                                          e.g., 'present' or 'removed'
+                                        type: string
+                                      linked_to_cluster:
+                                        description: Optional string containing a
+                                          cluster ID.     If set, the resource is
+                                          linked to the cluster and will be automatically
+                                          removed     when the cluster is removed
+                                        type: string
+                                      resource_type:
+                                        description: ceph.smb.join.auth
+                                        type: string
+                                    required:
+                                    - resource_type
+                                    - auth_id
+                                    - intent
+                                    - auth
+                                    - linked_to_cluster
+                                    type: object
+                                  state:
+                                    description: The current state of the resource,                        e.g.,
+                                      'created', 'updated', 'deleted'
+                                    type: string
+                                  success:
+                                    description: Indicates if the operation was successful
+                                    type: boolean
+                                required:
+                                - resource
+                                - state
+                                - success
+                                type: object
+                              type: array
+                            success:
+                              description: Indicates if the overall operation was
+                                successful
+                              type: boolean
+                          required:
+                          - results
+                          - success
+                          type: object
+                        state:
+                          description: The current state of the resource,                        e.g.,
+                            'created', 'updated', 'deleted'
+                          type: string
+                        success:
+                          description: Indicates if the operation was successful
+                          type: boolean
+                      required:
+                      - resource
+                      - state
+                      - success
+                      type: object
+                    type: array
+                  success:
+                    description: Indicates if the overall operation was successful
+                    type: boolean
+                required:
+                - results
+                - success
+                type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Create smb usersgroups
+      tags:
+      - SMB
+  /api/smb/usersgroups/{users_groups_id}:
+    delete:
+      description: "\n        Delete smb usersgroups resource\n\n        :param users_group_id:\
+        \ Users  identifier\n        :return: None.\n        "
+      parameters:
+      - description: users_groups_id
+        in: path
+        name: users_groups_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                properties: {}
+                type: object
+          description: Resource deleted.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Delete smb join auth
+      tags:
+      - SMB
+    get:
+      description: "\n        Get Users and groups resource\n\n        :return: Returns\
+        \ join auth.\n        :rtype: Dict\n        "
+      parameters:
+      - in: path
+        name: users_groups_id
+        required: true
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              schema:
+                properties:
+                  intent:
+                    description: Desired state of the resource, e.g., 'present' or
+                      'removed'
+                    type: string
+                  linked_to_cluster:
+                    description: Optional string containing a cluster ID.     If set,
+                      the resource is linked to the cluster and will be automatically
+                      removed     when the cluster is removed
+                    type: string
+                  resource_type:
+                    description: ceph.smb.usersgroups
+                    type: string
+                  users_groups_id:
+                    description: A short string identifying the usersgroups resource
+                    type: string
+                  values:
+                    description: Required object containing users and groups information
+                    properties:
+                      groups:
+                        description: List of group objects, each containing a name
+                        items:
+                          properties:
+                            name:
+                              description: The name of the group
+                              type: string
+                          required:
+                          - name
+                          type: object
+                        type: array
+                      users:
+                        description: List of user objects, each containing a name
+                          and password
+                        items:
+                          properties:
+                            name:
+                              description: The user name
+                              type: string
+                            password:
+                              description: The password for the user
+                              type: string
+                          required:
+                          - name
+                          - password
+                          type: object
+                        type: array
+                    required:
+                    - users
+                    - groups
+                    type: object
+                required:
+                - resource_type
+                - users_groups_id
+                - intent
+                - values
+                - linked_to_cluster
+                type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      summary: Get smb usersgroups authorization resource
+      tags:
+      - SMB
   /api/summary:
     get:
       parameters: []
index 8160f40d3df580fff5d591078e1e65b38abd6095..28a0db031a909d2f8fa00be5bb56fedb7ee88f6a 100644 (file)
@@ -265,6 +265,39 @@ class SMBJoinAuthTest(ControllerTestCase):
         self.assertStatus(200)
         self.assertJsonBody(self._join_auths['resources'])
 
+    def test_create_join_auth(self):
+        mock_simplified = Mock()
+        mock_simplified.to_simplified.return_value = json.dumps(self._join_auths['resources'][0])
+        mgr.remote = Mock(return_value=mock_simplified)
+
+        _join_auth_data = {'join_auth': self._join_auths['resources'][0]}
+
+        self._post(self._endpoint, _join_auth_data)
+        self.assertStatus(201)
+        self.assertInJsonBody(json.dumps(self._join_auths['resources'][0]))
+
+    def test_delete(self):
+        _res = {
+            "resource_type": "ceph.smb.join.auth",
+            "auth_id": "join1-admin",
+            "intent": "removed",
+            "auth": {
+                "username": "Administrator",
+                "password": "Passw0rd"
+            }
+        }
+        _res_simplified = {
+            "resource_type": "ceph.smb.join.auth",
+            "auth_id": "join1-admin",
+            "intent": "removed"
+        }
+
+        mgr.remote = Mock(return_value=Mock(return_value=_res))
+        mgr.remote.return_value.one.return_value.to_simplified = Mock(return_value=_res)
+        self._delete(f'{self._endpoint}/join1-admin')
+        self.assertStatus(204)
+        mgr.remote.assert_called_once_with('smb', 'apply_resources', json.dumps(_res_simplified))
+
 
 class SMBUsersgroupsTest(ControllerTestCase):
     _endpoint = '/api/smb/usersgroups'
@@ -319,3 +352,36 @@ class SMBUsersgroupsTest(ControllerTestCase):
         self._get(self._endpoint)
         self.assertStatus(200)
         self.assertJsonBody(self._usersgroups['resources'])
+
+    def test_create_usersgroups(self):
+        mock_simplified = Mock()
+        mock_simplified.to_simplified.return_value = json.dumps(self._usersgroups['resources'][0])
+        mgr.remote = Mock(return_value=mock_simplified)
+
+        _usersgroups_data = {'usersgroups': self._usersgroups['resources'][0]}
+
+        self._post(self._endpoint, _usersgroups_data)
+        self.assertStatus(201)
+        self.assertInJsonBody(json.dumps(self._usersgroups['resources'][0]))
+
+    def test_delete(self):
+        _res = {
+            "resource_type": "ceph.smb.usersgroups",
+            "users_groups_id": "ug1",
+            "intent": "removed",
+            "auth": {
+                "username": "Administrator",
+                "password": "Passw0rd"
+            }
+        }
+        _res_simplified = {
+            "resource_type": "ceph.smb.usersgroups",
+            "users_groups_id": "ug1",
+            "intent": "removed"
+        }
+
+        mgr.remote = Mock(return_value=Mock(return_value=_res))
+        mgr.remote.return_value.one.return_value.to_simplified = Mock(return_value=_res)
+        self._delete(f'{self._endpoint}/ug1')
+        self.assertStatus(204)
+        mgr.remote.assert_called_once_with('smb', 'apply_resources', json.dumps(_res_simplified))