]>
Commit | Line | Data |
---|---|---|
b3b6e05e | 1 | import logging |
b3b6e05e TL |
2 | import json |
3 | import re | |
a4b75251 TL |
4 | import socket |
5 | from typing import cast, Dict, List, Any, Union, Optional, TYPE_CHECKING, Tuple | |
b3b6e05e | 6 | |
a4b75251 | 7 | from mgr_module import NFS_POOL_NAME as POOL_NAME |
b3b6e05e TL |
8 | from ceph.deployment.service_spec import NFSServiceSpec, PlacementSpec, IngressSpec |
9 | ||
10 | import orchestrator | |
11 | ||
12 | from .exception import NFSInvalidOperation, ClusterNotFound | |
a4b75251 | 13 | from .utils import available_clusters, restart_nfs_service |
b3b6e05e TL |
14 | from .export import NFSRados, exception_handler |
15 | ||
a4b75251 TL |
16 | if TYPE_CHECKING: |
17 | from nfs.module import Module | |
18 | from mgr_module import MgrModule | |
19 | ||
20 | ||
b3b6e05e TL |
21 | log = logging.getLogger(__name__) |
22 | ||
23 | ||
24 | def resolve_ip(hostname: str) -> str: | |
25 | try: | |
26 | r = socket.getaddrinfo(hostname, None, flags=socket.AI_CANONNAME, | |
27 | type=socket.SOCK_STREAM) | |
28 | # pick first v4 IP, if present | |
29 | for a in r: | |
30 | if a[0] == socket.AF_INET: | |
31 | return a[4][0] | |
32 | return r[0][4][0] | |
33 | except socket.gaierror as e: | |
34 | raise NFSInvalidOperation(f"Cannot resolve IP for host {hostname}: {e}") | |
35 | ||
36 | ||
a4b75251 | 37 | def create_ganesha_pool(mgr: 'MgrModule') -> None: |
b3b6e05e | 38 | pool_list = [p['pool_name'] for p in mgr.get_osdmap().dump().get('pools', [])] |
a4b75251 TL |
39 | if POOL_NAME not in pool_list: |
40 | mgr.check_mon_command({'prefix': 'osd pool create', 'pool': POOL_NAME}) | |
b3b6e05e | 41 | mgr.check_mon_command({'prefix': 'osd pool application enable', |
a4b75251 | 42 | 'pool': POOL_NAME, |
b3b6e05e | 43 | 'app': 'nfs'}) |
a4b75251 | 44 | log.debug("Successfully created nfs-ganesha pool %s", POOL_NAME) |
b3b6e05e TL |
45 | |
46 | ||
47 | class NFSCluster: | |
a4b75251 | 48 | def __init__(self, mgr: 'Module') -> None: |
b3b6e05e TL |
49 | self.mgr = mgr |
50 | ||
a4b75251 TL |
51 | def _get_common_conf_obj_name(self, cluster_id: str) -> str: |
52 | return f'conf-nfs.{cluster_id}' | |
b3b6e05e | 53 | |
a4b75251 TL |
54 | def _get_user_conf_obj_name(self, cluster_id: str) -> str: |
55 | return f'userconf-nfs.{cluster_id}' | |
b3b6e05e | 56 | |
a4b75251 TL |
57 | def _call_orch_apply_nfs( |
58 | self, | |
59 | cluster_id: str, | |
60 | placement: Optional[str], | |
61 | virtual_ip: Optional[str] = None, | |
62 | port: Optional[int] = None, | |
63 | ) -> None: | |
64 | if not port: | |
65 | port = 2049 # default nfs port | |
b3b6e05e TL |
66 | if virtual_ip: |
67 | # nfs + ingress | |
68 | # run NFS on non-standard port | |
a4b75251 | 69 | spec = NFSServiceSpec(service_type='nfs', service_id=cluster_id, |
b3b6e05e TL |
70 | placement=PlacementSpec.from_string(placement), |
71 | # use non-default port so we don't conflict with ingress | |
a4b75251 | 72 | port=10000 + port) # semi-arbitrary, fix me someday |
b3b6e05e TL |
73 | completion = self.mgr.apply_nfs(spec) |
74 | orchestrator.raise_if_exception(completion) | |
75 | ispec = IngressSpec(service_type='ingress', | |
a4b75251 TL |
76 | service_id='nfs.' + cluster_id, |
77 | backend_service='nfs.' + cluster_id, | |
78 | frontend_port=port, | |
79 | monitor_port=7000 + port, # semi-arbitrary, fix me someday | |
b3b6e05e TL |
80 | virtual_ip=virtual_ip) |
81 | completion = self.mgr.apply_ingress(ispec) | |
82 | orchestrator.raise_if_exception(completion) | |
83 | else: | |
84 | # standalone nfs | |
a4b75251 TL |
85 | spec = NFSServiceSpec(service_type='nfs', service_id=cluster_id, |
86 | placement=PlacementSpec.from_string(placement), | |
87 | port=port) | |
b3b6e05e TL |
88 | completion = self.mgr.apply_nfs(spec) |
89 | orchestrator.raise_if_exception(completion) | |
a4b75251 | 90 | log.debug("Successfully deployed nfs daemons with cluster id %s and placement %s", cluster_id, placement) |
b3b6e05e | 91 | |
a4b75251 TL |
92 | def create_empty_rados_obj(self, cluster_id: str) -> None: |
93 | common_conf = self._get_common_conf_obj_name(cluster_id) | |
94 | NFSRados(self.mgr, cluster_id).write_obj('', self._get_common_conf_obj_name(cluster_id)) | |
95 | log.info("Created empty object:%s", common_conf) | |
96 | ||
97 | def delete_config_obj(self, cluster_id: str) -> None: | |
98 | NFSRados(self.mgr, cluster_id).remove_all_obj() | |
99 | log.info("Deleted %s object and all objects in %s", | |
100 | self._get_common_conf_obj_name(cluster_id), cluster_id) | |
101 | ||
102 | def create_nfs_cluster( | |
103 | self, | |
104 | cluster_id: str, | |
105 | placement: Optional[str], | |
106 | virtual_ip: Optional[str], | |
107 | ingress: Optional[bool] = None, | |
108 | port: Optional[int] = None, | |
109 | ) -> Tuple[int, str, str]: | |
b3b6e05e TL |
110 | try: |
111 | if virtual_ip and not ingress: | |
112 | raise NFSInvalidOperation('virtual_ip can only be provided with ingress enabled') | |
113 | if not virtual_ip and ingress: | |
114 | raise NFSInvalidOperation('ingress currently requires a virtual_ip') | |
115 | invalid_str = re.search('[^A-Za-z0-9-_.]', cluster_id) | |
116 | if invalid_str: | |
117 | raise NFSInvalidOperation(f"cluster id {cluster_id} is invalid. " | |
118 | f"{invalid_str.group()} is char not permitted") | |
119 | ||
a4b75251 | 120 | create_ganesha_pool(self.mgr) |
b3b6e05e | 121 | |
a4b75251 | 122 | self.create_empty_rados_obj(cluster_id) |
b3b6e05e TL |
123 | |
124 | if cluster_id not in available_clusters(self.mgr): | |
a4b75251 | 125 | self._call_orch_apply_nfs(cluster_id, placement, virtual_ip, port) |
b3b6e05e TL |
126 | return 0, "NFS Cluster Created Successfully", "" |
127 | return 0, "", f"{cluster_id} cluster already exists" | |
128 | except Exception as e: | |
129 | return exception_handler(e, f"NFS Cluster {cluster_id} could not be created") | |
130 | ||
a4b75251 | 131 | def delete_nfs_cluster(self, cluster_id: str) -> Tuple[int, str, str]: |
b3b6e05e TL |
132 | try: |
133 | cluster_list = available_clusters(self.mgr) | |
134 | if cluster_id in cluster_list: | |
135 | self.mgr.export_mgr.delete_all_exports(cluster_id) | |
a4b75251 | 136 | completion = self.mgr.remove_service('ingress.nfs.' + cluster_id) |
b3b6e05e | 137 | orchestrator.raise_if_exception(completion) |
a4b75251 | 138 | completion = self.mgr.remove_service('nfs.' + cluster_id) |
b3b6e05e | 139 | orchestrator.raise_if_exception(completion) |
a4b75251 | 140 | self.delete_config_obj(cluster_id) |
b3b6e05e TL |
141 | return 0, "NFS Cluster Deleted Successfully", "" |
142 | return 0, "", "Cluster does not exist" | |
143 | except Exception as e: | |
144 | return exception_handler(e, f"Failed to delete NFS Cluster {cluster_id}") | |
145 | ||
a4b75251 | 146 | def list_nfs_cluster(self) -> Tuple[int, str, str]: |
b3b6e05e TL |
147 | try: |
148 | return 0, '\n'.join(available_clusters(self.mgr)), "" | |
149 | except Exception as e: | |
150 | return exception_handler(e, "Failed to list NFS Cluster") | |
151 | ||
152 | def _show_nfs_cluster_info(self, cluster_id: str) -> Dict[str, Any]: | |
b3b6e05e | 153 | completion = self.mgr.list_daemons(daemon_type='nfs') |
b3b6e05e | 154 | # Here completion.result is a list DaemonDescription objects |
a4b75251 TL |
155 | clusters = orchestrator.raise_if_exception(completion) |
156 | backends: List[Dict[str, Union[Any]]] = [] | |
157 | ||
158 | for cluster in clusters: | |
159 | if cluster_id == cluster.service_id(): | |
160 | assert cluster.hostname | |
b3b6e05e TL |
161 | try: |
162 | if cluster.ip: | |
163 | ip = cluster.ip | |
164 | else: | |
165 | c = self.mgr.get_hosts() | |
166 | orchestrator.raise_if_exception(c) | |
a4b75251 | 167 | hosts = [h for h in c.result or [] |
b3b6e05e TL |
168 | if h.hostname == cluster.hostname] |
169 | if hosts: | |
170 | ip = resolve_ip(hosts[0].addr) | |
171 | else: | |
172 | # sigh | |
173 | ip = resolve_ip(cluster.hostname) | |
174 | backends.append({ | |
a4b75251 TL |
175 | "hostname": cluster.hostname, |
176 | "ip": ip, | |
177 | "port": cluster.ports[0] if cluster.ports else None | |
178 | }) | |
b3b6e05e TL |
179 | except orchestrator.OrchestratorError: |
180 | continue | |
181 | ||
182 | r: Dict[str, Any] = { | |
183 | 'virtual_ip': None, | |
184 | 'backend': backends, | |
185 | } | |
186 | sc = self.mgr.describe_service(service_type='ingress') | |
a4b75251 TL |
187 | services = orchestrator.raise_if_exception(sc) |
188 | for i in services: | |
b3b6e05e TL |
189 | spec = cast(IngressSpec, i.spec) |
190 | if spec.backend_service == f'nfs.{cluster_id}': | |
a4b75251 | 191 | r['virtual_ip'] = i.virtual_ip.split('/')[0] if i.virtual_ip else None |
b3b6e05e TL |
192 | if i.ports: |
193 | r['port'] = i.ports[0] | |
194 | if len(i.ports) > 1: | |
195 | r['monitor_port'] = i.ports[1] | |
a4b75251 | 196 | log.debug("Successfully fetched %s info: %s", cluster_id, r) |
b3b6e05e TL |
197 | return r |
198 | ||
a4b75251 | 199 | def show_nfs_cluster_info(self, cluster_id: Optional[str] = None) -> Tuple[int, str, str]: |
b3b6e05e | 200 | try: |
b3b6e05e TL |
201 | info_res = {} |
202 | if cluster_id: | |
203 | cluster_ls = [cluster_id] | |
204 | else: | |
205 | cluster_ls = available_clusters(self.mgr) | |
206 | ||
207 | for cluster_id in cluster_ls: | |
208 | res = self._show_nfs_cluster_info(cluster_id) | |
209 | if res: | |
210 | info_res[cluster_id] = res | |
211 | return (0, json.dumps(info_res, indent=4), '') | |
212 | except Exception as e: | |
213 | return exception_handler(e, "Failed to show info for cluster") | |
214 | ||
a4b75251 TL |
215 | def get_nfs_cluster_config(self, cluster_id: str) -> Tuple[int, str, str]: |
216 | try: | |
217 | if cluster_id in available_clusters(self.mgr): | |
218 | rados_obj = NFSRados(self.mgr, cluster_id) | |
219 | conf = rados_obj.read_obj(self._get_user_conf_obj_name(cluster_id)) | |
220 | return 0, conf or "", "" | |
221 | raise ClusterNotFound() | |
222 | except Exception as e: | |
223 | return exception_handler(e, f"Fetching NFS-Ganesha Config failed for {cluster_id}") | |
224 | ||
225 | def set_nfs_cluster_config(self, cluster_id: str, nfs_config: str) -> Tuple[int, str, str]: | |
b3b6e05e | 226 | try: |
b3b6e05e | 227 | if cluster_id in available_clusters(self.mgr): |
a4b75251 | 228 | rados_obj = NFSRados(self.mgr, cluster_id) |
b3b6e05e TL |
229 | if rados_obj.check_user_config(): |
230 | return 0, "", "NFS-Ganesha User Config already exists" | |
a4b75251 TL |
231 | rados_obj.write_obj(nfs_config, self._get_user_conf_obj_name(cluster_id), |
232 | self._get_common_conf_obj_name(cluster_id)) | |
233 | log.debug("Successfully saved %s's user config: \n %s", cluster_id, nfs_config) | |
b3b6e05e TL |
234 | restart_nfs_service(self.mgr, cluster_id) |
235 | return 0, "NFS-Ganesha Config Set Successfully", "" | |
236 | raise ClusterNotFound() | |
237 | except NotImplementedError: | |
238 | return 0, "NFS-Ganesha Config Added Successfully "\ | |
a4b75251 | 239 | "(Manual Restart of NFS PODS required)", "" |
b3b6e05e TL |
240 | except Exception as e: |
241 | return exception_handler(e, f"Setting NFS-Ganesha Config failed for {cluster_id}") | |
242 | ||
a4b75251 | 243 | def reset_nfs_cluster_config(self, cluster_id: str) -> Tuple[int, str, str]: |
b3b6e05e TL |
244 | try: |
245 | if cluster_id in available_clusters(self.mgr): | |
a4b75251 | 246 | rados_obj = NFSRados(self.mgr, cluster_id) |
b3b6e05e TL |
247 | if not rados_obj.check_user_config(): |
248 | return 0, "", "NFS-Ganesha User Config does not exist" | |
a4b75251 TL |
249 | rados_obj.remove_obj(self._get_user_conf_obj_name(cluster_id), |
250 | self._get_common_conf_obj_name(cluster_id)) | |
b3b6e05e TL |
251 | restart_nfs_service(self.mgr, cluster_id) |
252 | return 0, "NFS-Ganesha Config Reset Successfully", "" | |
253 | raise ClusterNotFound() | |
254 | except NotImplementedError: | |
255 | return 0, "NFS-Ganesha Config Removed Successfully "\ | |
a4b75251 | 256 | "(Manual Restart of NFS PODS required)", "" |
b3b6e05e TL |
257 | except Exception as e: |
258 | return exception_handler(e, f"Resetting NFS-Ganesha Config failed for {cluster_id}") |