Passed
Push — master ( 9f382f...4cae1d )
by
unknown
04:16
created

BaseDatastoreService._get_key_name_with_prefix()   A

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
from datetime import timedelta
17
18
from oslo_config import cfg
19
20
from st2client.client import Client
21
from st2client.models import KeyValuePair
22
from st2common.util.api import get_full_public_api_url
23
from st2common.util.date import get_datetime_utc_now
24
from st2common.constants.keyvalue import DATASTORE_KEY_SEPARATOR, SYSTEM_SCOPE
25
26
__all__ = [
27
    'BaseDatastoreService',
28
    'ActionDatastoreService',
29
    'SensorDatastoreService'
30
]
31
32
33
class BaseDatastoreService(object):
34
    """
35
    Base DatastoreService class which provides public methods for accessing datastore items.
36
    """
37
38
    DATASTORE_NAME_SEPARATOR = DATASTORE_KEY_SEPARATOR
39
40
    def __init__(self, logger, pack_name, class_name):
41
        """
42
        :param auth_token: Auth token used to authenticate with StackStorm API.
43
        :type auth_token: ``str``
44
        """
45
        self._pack_name = pack_name
46
        self._class_name = class_name
47
        self._logger = logger
48
49
        self._client = None
50
        self._token_expire = get_datetime_utc_now()
51
52
    ##################################
53
    # Methods for datastore management
54
    ##################################
55
56
    def list_values(self, local=True, prefix=None):
57
        """
58
        Retrieve all the datastores items.
59
60
        :param local: List values from a namespace local to this pack/class. Defaults to True.
61
        :type: local: ``bool``
62
63
        :param prefix: Optional key name prefix / startswith filter.
64
        :type prefix: ``str``
65
66
        :rtype: ``list`` of :class:`KeyValuePair`
67
        """
68
        client = self._get_api_client()
69
        self._logger.audit('Retrieving all the value from the datastore')
70
71
        key_prefix = self._get_full_key_prefix(local=local, prefix=prefix)
72
        kvps = client.keys.get_all(prefix=key_prefix)
73
        return kvps
74
75
    def get_value(self, name, local=True, scope=SYSTEM_SCOPE, decrypt=False):
76
        """
77
        Retrieve a value from the datastore for the provided key.
78
79
        By default, value is retrieved from the namespace local to the pack/class. If you want to
80
        retrieve a global value from a datastore, pass local=False to this method.
81
82
        :param name: Key name.
83
        :type name: ``str``
84
85
        :param local: Retrieve value from a namespace local to the pack/class. Defaults to True.
86
        :type: local: ``bool``
87
88
        :param scope: Scope under which item is saved. Defaults to system scope.
89
        :type: local: ``str``
90
91
        :param decrypt: Return the decrypted value. Defaults to False.
92
        :type: local: ``bool``
93
94
        :rtype: ``str`` or ``None``
95
        """
96
        if scope != SYSTEM_SCOPE:
97
            raise ValueError('Scope %s is unsupported.' % scope)
98
99
        name = self._get_full_key_name(name=name, local=local)
100
101
        client = self._get_api_client()
102
        self._logger.audit('Retrieving value from the datastore (name=%s)', name)
103
104
        try:
105
            params = {'decrypt': str(decrypt).lower(), 'scope': scope}
106
            kvp = client.keys.get_by_id(id=name, params=params)
107
        except Exception as e:
108
            self._logger.exception(
109
                'Exception retrieving value from datastore (name=%s): %s',
110
                name,
111
                e
112
            )
113
            return None
114
115
        if kvp:
116
            return kvp.value
117
118
        return None
119
120
    def set_value(self, name, value, ttl=None, local=True, scope=SYSTEM_SCOPE, encrypt=False):
121
        """
122
        Set a value for the provided key.
123
124
        By default, value is set in a namespace local to the pack/class. If you want to
125
        set a global value, pass local=False to this method.
126
127
        :param name: Key name.
128
        :type name: ``str``
129
130
        :param value: Key value.
131
        :type value: ``str``
132
133
        :param ttl: Optional TTL (in seconds).
134
        :type ttl: ``int``
135
136
        :param local: Set value in a namespace local to the pack/class. Defaults to True.
137
        :type: local: ``bool``
138
139
        :param scope: Scope under which to place the item. Defaults to system scope.
140
        :type: local: ``str``
141
142
        :param encrypt: Encrypt the value when saving. Defaults to False.
143
        :type: local: ``bool``
144
145
        :return: ``True`` on success, ``False`` otherwise.
146
        :rtype: ``bool``
147
        """
148
        if scope != SYSTEM_SCOPE:
149
            raise ValueError('Scope %s is unsupported.', scope)
150
151
        name = self._get_full_key_name(name=name, local=local)
152
153
        value = str(value)
154
        client = self._get_api_client()
155
156
        self._logger.audit('Setting value in the datastore (name=%s)', name)
157
158
        instance = KeyValuePair()
159
        instance.id = name
160
        instance.name = name
161
        instance.value = value
162
        instance.scope = scope
163
        if encrypt:
164
            instance.secret = True
165
166
        if ttl:
167
            instance.ttl = ttl
168
169
        client.keys.update(instance=instance)
170
        return True
171
172
    def delete_value(self, name, local=True, scope=SYSTEM_SCOPE):
173
        """
174
        Delete the provided key.
175
176
        By default, value is deleted from a namespace local to the pack/class. If you want to
177
        delete a global value, pass local=False to this method.
178
179
        :param name: Name of the key to delete.
180
        :type name: ``str``
181
182
        :param local: Delete a value in a namespace local to the pack/class. Defaults to True.
183
        :type: local: ``bool``
184
185
        :param scope: Scope under which item is saved. Defaults to system scope.
186
        :type: local: ``str``
187
188
        :return: ``True`` on success, ``False`` otherwise.
189
        :rtype: ``bool``
190
        """
191
        if scope != SYSTEM_SCOPE:
192
            raise ValueError('Scope %s is unsupported.', scope)
193
194
        name = self._get_full_key_name(name=name, local=local)
195
196
        client = self._get_api_client()
197
198
        instance = KeyValuePair()
199
        instance.id = name
200
        instance.name = name
201
202
        self._logger.audit('Deleting value from the datastore (name=%s)', name)
203
204
        try:
205
            params = {'scope': scope}
206
            client.keys.delete(instance=instance, params=params)
207
        except Exception as e:
208
            self._logger.exception(
209
                'Exception deleting value from datastore (name=%s): %s',
210
                name,
211
                e
212
            )
213
            return False
214
215
        return True
216
217
    def _get_api_client(self):
218
        """
219
        Retrieve API client instance.
220
        """
221
        raise NotImplementedError('_get_api_client() not implemented')
222
223
    def _get_full_key_name(self, name, local):
224
        """
225
        Retrieve a full key name.
226
227
        :rtype: ``str``
228
        """
229
        if local:
230
            name = self._get_key_name_with_prefix(name=name)
231
232
        return name
233
234
    def _get_full_key_prefix(self, local, prefix=None):
235
        if local:
236
            key_prefix = self._get_local_key_name_prefix()
237
238
            if prefix:
239
                key_prefix += prefix
240
        else:
241
            key_prefix = prefix
242
243
        return key_prefix
244
245
    def _get_local_key_name_prefix(self):
246
        """
247
        Retrieve key prefix which is local to this pack/class.
248
        """
249
        key_prefix = self._get_datastore_key_prefix() + self.DATASTORE_NAME_SEPARATOR
250
        return key_prefix
251
252
    def _get_key_name_with_prefix(self, name):
253
        """
254
        Retrieve a full key name which is local to the current pack/class.
255
256
        :param name: Base datastore key name.
257
        :type name: ``str``
258
259
        :rtype: ``str``
260
        """
261
        prefix = self._get_datastore_key_prefix()
262
        full_name = prefix + self.DATASTORE_NAME_SEPARATOR + name
263
        return full_name
264
265
    def _get_datastore_key_prefix(self):
266
        prefix = '%s.%s' % (self._pack_name, self._class_name)
267
        return prefix
268
269
270
class ActionDatastoreService(BaseDatastoreService):
271
    """
272
    DatastoreService class used by actions. This class uses temporary auth token which is generated
273
    by the runner container service and available for the duration of the lifetime of an action.
274
275
    Note: This class does NOT need database access.
276
    """
277
278
    def __init__(self, logger, pack_name, class_name, auth_token):
279
        """
280
        :param auth_token: Auth token used to authenticate with StackStorm API.
281
        :type auth_token: ``str``
282
        """
283
        super(ActionDatastoreService, self).__init__(logger=logger, pack_name=pack_name,
284
                                                     class_name=class_name)
285
286
        self._auth_token = auth_token
287
        self._client = None
288
289
    def _get_api_client(self):
290
        """
291
        Retrieve API client instance.
292
        """
293
        if not self._client:
294
            self._logger.audit('Creating new Client object.')
295
296
            api_url = get_full_public_api_url()
297
            client = Client(api_url=api_url, token=self._auth_token)
298
            self._client = client
299
300
        return self._client
301
302
303
class SensorDatastoreService(BaseDatastoreService):
304
    """
305
    DatastoreService class used by sensors. This class is meant to be used in context of long
306
    running processes (e.g. sensors) and it uses "create_token" method to generate a new auth
307
    token. A new token is also automatically generated once the old one expires.
308
309
    Note: This class does need database access (create_token method - to be able to create a new
310
    token).
311
    """
312
313
    def __init__(self, logger, pack_name, class_name, api_username):
314
        super(SensorDatastoreService, self).__init__(logger=logger, pack_name=pack_name,
315
                                                     class_name=class_name)
316
        self._api_username = api_username
317
        self._token_expire = get_datetime_utc_now()
318
319
    def _get_api_client(self):
320
        """
321
        Retrieve API client instance.
322
        """
323
        token_expire = self._token_expire <= get_datetime_utc_now()
324
325
        if not self._client or token_expire:
326
            # Note: Late import to avoid high import cost (time wise)
327
            from st2common.services.access import create_token
328
            self._logger.audit('Creating new Client object.')
329
330
            ttl = cfg.CONF.auth.service_token_ttl
331
            api_url = get_full_public_api_url()
332
333
            temporary_token = create_token(username=self._api_username, ttl=ttl, service=True)
334
            self._client = Client(api_url=api_url, token=temporary_token.token)
335
            self._token_expire = get_datetime_utc_now() + timedelta(seconds=ttl)
336
337
        return self._client
338