]>
Commit | Line | Data |
---|---|---|
f67539c2 | 1 | import errno |
e306af50 TL |
2 | import json |
3 | import logging | |
b3b6e05e | 4 | import subprocess |
f67539c2 TL |
5 | from typing import List, cast, Optional |
6 | from ipaddress import ip_address, IPv6Address | |
e306af50 | 7 | |
f67539c2 | 8 | from mgr_module import HandleCommandResult |
e306af50 TL |
9 | from ceph.deployment.service_spec import IscsiServiceSpec |
10 | ||
f67539c2 TL |
11 | from orchestrator import DaemonDescription, DaemonDescriptionStatus |
12 | from .cephadmservice import CephadmDaemonDeploySpec, CephService | |
e306af50 TL |
13 | from .. import utils |
14 | ||
15 | logger = logging.getLogger(__name__) | |
16 | ||
17 | ||
f91f0fd5 | 18 | class IscsiService(CephService): |
f6b5b4d7 TL |
19 | TYPE = 'iscsi' |
20 | ||
f67539c2 | 21 | def config(self, spec: IscsiServiceSpec, daemon_id: str) -> None: # type: ignore |
f6b5b4d7 | 22 | assert self.TYPE == spec.service_type |
adb31ebb | 23 | assert spec.pool |
e306af50 TL |
24 | self.mgr._check_pool_exists(spec.pool, spec.service_name()) |
25 | ||
f67539c2 | 26 | def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec: |
f6b5b4d7 | 27 | assert self.TYPE == daemon_spec.daemon_type |
f6b5b4d7 | 28 | |
f67539c2 | 29 | spec = cast(IscsiServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec) |
f6b5b4d7 TL |
30 | igw_id = daemon_spec.daemon_id |
31 | ||
f67539c2 TL |
32 | keyring = self.get_keyring_with_caps(self.get_auth_entity(igw_id), |
33 | ['mon', 'profile rbd, ' | |
34 | 'allow command "osd blocklist", ' | |
35 | 'allow command "config-key get" with "key" prefix "iscsi/"', | |
36 | 'mgr', 'allow command "service status"', | |
37 | 'osd', 'allow rwx']) | |
e306af50 TL |
38 | |
39 | if spec.ssl_cert: | |
40 | if isinstance(spec.ssl_cert, list): | |
41 | cert_data = '\n'.join(spec.ssl_cert) | |
42 | else: | |
43 | cert_data = spec.ssl_cert | |
f91f0fd5 | 44 | ret, out, err = self.mgr.check_mon_command({ |
e306af50 TL |
45 | 'prefix': 'config-key set', |
46 | 'key': f'iscsi/{utils.name_to_config_section("iscsi")}.{igw_id}/iscsi-gateway.crt', | |
47 | 'val': cert_data, | |
48 | }) | |
49 | ||
50 | if spec.ssl_key: | |
51 | if isinstance(spec.ssl_key, list): | |
52 | key_data = '\n'.join(spec.ssl_key) | |
53 | else: | |
54 | key_data = spec.ssl_key | |
f91f0fd5 | 55 | ret, out, err = self.mgr.check_mon_command({ |
e306af50 TL |
56 | 'prefix': 'config-key set', |
57 | 'key': f'iscsi/{utils.name_to_config_section("iscsi")}.{igw_id}/iscsi-gateway.key', | |
58 | 'val': key_data, | |
59 | }) | |
60 | ||
61 | context = { | |
62 | 'client_name': '{}.{}'.format(utils.name_to_config_section('iscsi'), igw_id), | |
63 | 'spec': spec | |
64 | } | |
65 | igw_conf = self.mgr.template.render('services/iscsi/iscsi-gateway.cfg.j2', context) | |
f6b5b4d7 TL |
66 | |
67 | daemon_spec.keyring = keyring | |
f91f0fd5 | 68 | daemon_spec.extra_files = {'iscsi-gateway.cfg': igw_conf} |
f6b5b4d7 | 69 | |
f67539c2 TL |
70 | daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec) |
71 | ||
f91f0fd5 | 72 | return daemon_spec |
f6b5b4d7 | 73 | |
f91f0fd5 | 74 | def config_dashboard(self, daemon_descrs: List[DaemonDescription]) -> None: |
f6b5b4d7 TL |
75 | def get_set_cmd_dicts(out: str) -> List[dict]: |
76 | gateways = json.loads(out)['gateways'] | |
77 | cmd_dicts = [] | |
f67539c2 | 78 | # TODO: fail, if we don't have a spec |
adb31ebb | 79 | spec = cast(IscsiServiceSpec, |
f67539c2 | 80 | self.mgr.spec_store.all_specs.get(daemon_descrs[0].service_name(), None)) |
adb31ebb TL |
81 | if spec.api_secure and spec.ssl_cert and spec.ssl_key: |
82 | cmd_dicts.append({ | |
83 | 'prefix': 'dashboard set-iscsi-api-ssl-verification', | |
84 | 'value': "false" | |
85 | }) | |
86 | else: | |
87 | cmd_dicts.append({ | |
88 | 'prefix': 'dashboard set-iscsi-api-ssl-verification', | |
89 | 'value': "true" | |
90 | }) | |
f6b5b4d7 | 91 | for dd in daemon_descrs: |
f67539c2 TL |
92 | assert dd.hostname is not None |
93 | # todo: this can fail: | |
f6b5b4d7 | 94 | spec = cast(IscsiServiceSpec, |
f67539c2 | 95 | self.mgr.spec_store.all_specs.get(dd.service_name(), None)) |
f6b5b4d7 TL |
96 | if not spec: |
97 | logger.warning('No ServiceSpec found for %s', dd) | |
98 | continue | |
b3b6e05e | 99 | ip = utils.resolve_ip(self.mgr.inventory.get_addr(dd.hostname)) |
f67539c2 TL |
100 | # IPv6 URL encoding requires square brackets enclosing the ip |
101 | if type(ip_address(ip)) is IPv6Address: | |
102 | ip = f'[{ip}]' | |
adb31ebb TL |
103 | protocol = "http" |
104 | if spec.api_secure and spec.ssl_cert and spec.ssl_key: | |
105 | protocol = "https" | |
106 | service_url = '{}://{}:{}@{}:{}'.format( | |
107 | protocol, spec.api_user or 'admin', spec.api_password or 'admin', ip, spec.api_port or '5000') | |
108 | gw = gateways.get(dd.hostname) | |
f6b5b4d7 | 109 | if not gw or gw['service_url'] != service_url: |
adb31ebb TL |
110 | safe_service_url = '{}://{}:{}@{}:{}'.format( |
111 | protocol, '<api-user>', '<api-password>', ip, spec.api_port or '5000') | |
112 | logger.info('Adding iSCSI gateway %s to Dashboard', safe_service_url) | |
f6b5b4d7 | 113 | cmd_dicts.append({ |
e306af50 | 114 | 'prefix': 'dashboard iscsi-gateway-add', |
cd265ab1 | 115 | 'inbuf': service_url, |
adb31ebb | 116 | 'name': dd.hostname |
e306af50 | 117 | }) |
f6b5b4d7 TL |
118 | return cmd_dicts |
119 | ||
120 | self._check_and_set_dashboard( | |
121 | service_name='iSCSI', | |
122 | get_cmd='dashboard iscsi-gateway-list', | |
123 | get_set_cmd_dicts=get_set_cmd_dicts | |
124 | ) | |
f67539c2 TL |
125 | |
126 | def ok_to_stop(self, | |
127 | daemon_ids: List[str], | |
128 | force: bool = False, | |
129 | known: Optional[List[str]] = None) -> HandleCommandResult: | |
130 | # if only 1 iscsi, alert user (this is not passable with --force) | |
131 | warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'Iscsi', 1, True) | |
132 | if warn: | |
133 | return HandleCommandResult(-errno.EBUSY, '', warn_message) | |
134 | ||
135 | # if reached here, there is > 1 nfs daemon. make sure none are down | |
136 | warn_message = ( | |
137 | 'ALERT: 1 iscsi daemon is already down. Please bring it back up before stopping this one') | |
138 | iscsi_daemons = self.mgr.cache.get_daemons_by_type(self.TYPE) | |
139 | for i in iscsi_daemons: | |
140 | if i.status != DaemonDescriptionStatus.running: | |
141 | return HandleCommandResult(-errno.EBUSY, '', warn_message) | |
142 | ||
143 | names = [f'{self.TYPE}.{d_id}' for d_id in daemon_ids] | |
144 | warn_message = f'It is presumed safe to stop {names}' | |
145 | return HandleCommandResult(0, warn_message, '') | |
b3b6e05e TL |
146 | |
147 | def post_remove(self, daemon: DaemonDescription) -> None: | |
148 | """ | |
149 | Called after the daemon is removed. | |
150 | """ | |
151 | logger.debug(f'Post remove daemon {self.TYPE}.{daemon.daemon_id}') | |
152 | ||
153 | # remove config for dashboard iscsi gateways | |
154 | ret, out, err = self.mgr.check_mon_command({ | |
155 | 'prefix': 'dashboard iscsi-gateway-rm', | |
156 | 'name': daemon.hostname, | |
157 | }) | |
158 | logger.info(f'{daemon.hostname} removed from iscsi gateways dashboard config') | |
159 | ||
160 | # needed to know if we have ssl stuff for iscsi in ceph config | |
161 | iscsi_config_dict = {} | |
162 | ret, iscsi_config, err = self.mgr.check_mon_command({ | |
163 | 'prefix': 'config-key dump', | |
164 | 'key': 'iscsi', | |
165 | }) | |
166 | if iscsi_config: | |
167 | iscsi_config_dict = json.loads(iscsi_config) | |
168 | ||
169 | # remove iscsi cert and key from ceph config | |
170 | for iscsi_key, value in iscsi_config_dict.items(): | |
171 | if f'iscsi/client.{daemon.name()}/' in iscsi_key: | |
172 | ret, out, err = self.mgr.check_mon_command({ | |
173 | 'prefix': 'config-key rm', | |
174 | 'key': iscsi_key, | |
175 | }) | |
176 | logger.info(f'{iscsi_key} removed from ceph config') | |
177 | ||
178 | def purge(self, service_name: str) -> None: | |
179 | """Removes configuration | |
180 | """ | |
181 | spec = cast(IscsiServiceSpec, self.mgr.spec_store[service_name].spec) | |
182 | try: | |
183 | # remove service configuration from the pool | |
184 | try: | |
185 | subprocess.run(['rados', | |
186 | '-k', str(self.mgr.get_ceph_option('keyring')), | |
187 | '-n', f'mgr.{self.mgr.get_mgr_id()}', | |
188 | '-p', cast(str, spec.pool), | |
189 | 'rm', | |
190 | 'gateway.conf'], | |
191 | timeout=5) | |
192 | logger.info(f'<gateway.conf> removed from {spec.pool}') | |
193 | except subprocess.CalledProcessError as ex: | |
194 | logger.error(f'Error executing <<{ex.cmd}>>: {ex.output}') | |
195 | except subprocess.TimeoutExpired: | |
196 | logger.error(f'timeout (5s) trying to remove <gateway.conf> from {spec.pool}') | |
197 | ||
198 | except Exception: | |
199 | logger.exception(f'failed to purge {service_name}') |