summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/services/nvmeof_conf.py
blob: 170f98c70d18f2e69c888b40f32cb1be098381f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# -*- coding: utf-8 -*-

import json
import logging

from orchestrator import OrchestratorError

from .. import mgr
from ..exceptions import DashboardException
from ..services.orchestrator import OrchClient

logger = logging.getLogger('nvmeof_conf')


class NvmeofGatewayAlreadyExists(Exception):
    def __init__(self, gateway_name):
        super(NvmeofGatewayAlreadyExists, self).__init__(
            "NVMe-oF gateway '{}' already exists".format(gateway_name))


class NvmeofGatewayDoesNotExist(Exception):
    def __init__(self, hostname):
        super(NvmeofGatewayDoesNotExist, self).__init__(
            "NVMe-oF gateway '{}' does not exist".format(hostname))


class ManagedByOrchestratorException(Exception):
    def __init__(self):
        super(ManagedByOrchestratorException, self).__init__(
            "NVMe-oF configuration is managed by the orchestrator")


_NVMEOF_STORE_KEY = "_nvmeof_config"


class NvmeofGatewaysConfig(object):
    @classmethod
    def _load_config_from_store(cls):
        json_db = mgr.get_store(_NVMEOF_STORE_KEY,
                                '{"gateways": {}}')
        config = json.loads(json_db)
        cls._save_config(config)
        return config

    @classmethod
    def _save_config(cls, config):
        mgr.set_store(_NVMEOF_STORE_KEY, json.dumps(config))

    @classmethod
    def get_gateways_config(cls):
        return cls._load_config_from_store()

    @classmethod
    def add_gateway(cls, name, service_url, group, daemon_name):
        config = cls.get_gateways_config()

        if name in config.get('gateways', {}):
            existing_gateways = config['gateways'][name]
            for gateway in existing_gateways:
                if 'daemon_name' not in gateway:
                    gateway['daemon_name'] = daemon_name
                    break
                if gateway['service_url'] == service_url:
                    return

        new_gateway = {
            'service_url': service_url,
            'group': group,
            'daemon_name': daemon_name
        }

        if name in config.get('gateways', {}):
            config['gateways'][name].append(new_gateway)
        else:
            config['gateways'][name] = [new_gateway]

        cls._save_config(config)

    @classmethod
    def remove_gateway(cls, name, daemon_name=None):
        config = cls.get_gateways_config()
        if name not in config['gateways']:
            raise NvmeofGatewayDoesNotExist(name)

        if not daemon_name:
            del config['gateways'][name]
        else:
            # remove the daemon from the list of gateways
            config['gateways'][name] = [daemon for daemon in config['gateways'][name]
                                        if daemon['daemon_name'] != daemon_name]

            # if there are no more daemons in the list, remove the gateway
            if not config['gateways'][name]:
                del config['gateways'][name]

        cls._save_config(config)

    @classmethod
    def get_service_info(cls, group=None):
        try:
            config = cls.get_gateways_config()
            gateways = config.get('gateways', {})
            if not gateways:
                return None

            if group:
                return _get_name_url_for_group(gateways, group)

            return _get_default_service(gateways)

        except (KeyError, IndexError) as e:
            raise DashboardException(
                msg=f'NVMe-oF configuration is not set: {e}',
            )

    @classmethod
    def get_client_cert(cls, service_name: str):
        client_cert = cls.from_cert_store('nvmeof_client_cert', service_name)
        return client_cert.encode() if client_cert else None

    @classmethod
    def get_client_key(cls, service_name: str):
        client_key = cls.from_cert_store('nvmeof_client_key', service_name, key=True)
        return client_key.encode() if client_key else None

    @classmethod
    def get_root_ca_cert(cls, service_name: str):
        root_ca_cert = cls.from_cert_store('nvmeof_root_ca_cert', service_name)
        # If root_ca_cert is not set, use server_cert as root_ca_cert
        return root_ca_cert.encode() if root_ca_cert else cls.get_server_cert(service_name)

    @classmethod
    def get_server_cert(cls, service_name: str):
        server_cert = cls.from_cert_store('nvmeof_server_cert', service_name)
        return server_cert.encode() if server_cert else None

    @classmethod
    def from_cert_store(cls, entity: str, service_name: str, key=False):
        try:
            orch = OrchClient.instance()
            if orch.available():
                if key:
                    return orch.cert_store.get_key(entity, service_name,
                                                   ignore_missing_exception=True)
                return orch.cert_store.get_cert(entity, service_name,
                                                ignore_missing_exception=True)
            return None
        except OrchestratorError:
            # just return None if any orchestrator error is raised
            # otherwise nvmeof api will raise this error and doesn't proceed.
            return None


def _get_name_url_for_group(gateways, group):
    try:
        orch = OrchClient.instance()
        for service_name, svc_config in gateways.items():
            # get the group name of the service and match it against the
            # group name provided
            group_name_from_svc = orch.services.get(service_name)[0].spec.group
            if group == group_name_from_svc:
                running_daemons = _get_running_daemons(orch, service_name)
                config = _get_running_daemon_svc_config(svc_config, running_daemons)

                if config:
                    return service_name, config['service_url']
        return None

    except OrchestratorError:
        return _get_default_service(gateways)


def _get_running_daemons(orch, service_name):
    # get the running nvmeof daemons
    daemons = [d.to_dict()
               for d in orch.services.list_daemons(service_name=service_name)]
    return [d['daemon_name'] for d in daemons
            if d['status_desc'] == 'running']


def _get_running_daemon_svc_config(svc_config, running_daemons):
    try:
        return next(config for config in svc_config
                    if config['daemon_name'] in running_daemons)
    except StopIteration:
        return None


def _get_default_service(gateways):
    if gateways:
        gateway_keys = list(gateways.keys())
        # if there are more than 1 gateway, rather than chosing a random gateway
        # from any of the group, raise an exception to make it clear that we need
        # to specify the group name in the API request.
        if len(gateway_keys) > 1:
            raise DashboardException(
                msg=(
                    "Multiple NVMe-oF gateway groups are configured. "
                    "Please specify the 'gw_group' parameter in the request."
                ),
                component="nvmeof"
            )
        service_name = gateway_keys[0]
        return service_name, gateways[service_name][0]['service_url']
    return None