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