Test Failed
Pull Request — master (#4197)
by W
03:53
created

ActionsController.delete()   A

Complexity

Conditions 3

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
dl 0
loc 36
rs 9.016
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
import os
17
import os.path
18
19
import six
20
from mongoengine import ValidationError
21
22
# TODO: Encapsulate mongoengine errors in our persistence layer. Exceptions
23
#       that bubble up to this layer should be core Python exceptions or
24
#       StackStorm defined exceptions.
25
26
from st2api.controllers import resource
27
from st2api.controllers.v1.action_views import ActionViewsController
28
from st2common import log as logging
29
from st2common.constants.triggers import ACTION_FILE_WRITTEN_TRIGGER
30
from st2common.exceptions.action import InvalidActionParameterException
31
from st2common.exceptions.apivalidation import ValueValidationException
32
from st2common.persistence.action import Action
33
from st2common.models.api.action import ActionAPI
34
from st2common.persistence.pack import Pack
35
from st2common.rbac.types import PermissionType
36
from st2common.rbac import utils as rbac_utils
37
from st2common.router import abort
38
from st2common.router import Response
39
from st2common.validators.api.misc import validate_not_part_of_system_pack
40
from st2common.content.utils import get_pack_base_path
41
from st2common.content.utils import get_pack_resource_file_abs_path
42
from st2common.content.utils import get_relative_path_to_pack
43
from st2common.transport.reactor import TriggerDispatcher
44
from st2common.util.system_info import get_host_info
45
import st2common.validators.api.action as action_validator
46
47
http_client = six.moves.http_client
48
49
LOG = logging.getLogger(__name__)
50
51
52
class ActionsController(resource.ContentPackResourceController):
53
    """
54
        Implements the RESTful web endpoint that handles
55
        the lifecycle of Actions in the system.
56
    """
57
    views = ActionViewsController()
58
59
    model = ActionAPI
60
    access = Action
61
    supported_filters = {
62
        'name': 'name',
63
        'pack': 'pack'
64
    }
65
66
    query_options = {
67
        'sort': ['pack', 'name']
68
    }
69
70
    valid_exclude_attributes = [
71
        'parameters',
72
        'notify'
73
    ]
74
75
    include_reference = True
76
77
    def __init__(self, *args, **kwargs):
78
        super(ActionsController, self).__init__(*args, **kwargs)
79
        self._trigger_dispatcher = TriggerDispatcher(LOG)
80
81
    def get_all(self, exclude_attributes=None, include_attributes=None,
82
                sort=None, offset=0, limit=None,
83
                requester_user=None, **raw_filters):
84
        exclude_fields = self._validate_exclude_fields(exclude_attributes)
85
86
        if include_attributes:
87
            # Note: Those fields need to be always included for API model to work
88
            include_attributes += ['name', 'pack', 'runner_type']
89
90
        return super(ActionsController, self)._get_all(exclude_fields=exclude_fields,
91
                                                       include_fields=include_attributes,
92
                                                       sort=sort,
93
                                                       offset=offset,
94
                                                       limit=limit,
95
                                                       raw_filters=raw_filters,
96
                                                       requester_user=requester_user)
97
98
    def get_one(self, ref_or_id, requester_user):
99
        return super(ActionsController, self)._get_one(ref_or_id, requester_user=requester_user,
100
                                                       permission_type=PermissionType.ACTION_VIEW)
101
102
    def post(self, action, requester_user):
103
        """
104
            Create a new action.
105
106
            Handles requests:
107
                POST /actions/
108
        """
109
110
        permission_type = PermissionType.ACTION_CREATE
111
        rbac_utils.assert_user_has_resource_api_permission(user_db=requester_user,
112
                                                           resource_api=action,
113
                                                           permission_type=permission_type)
114
115
        try:
116
            # Perform validation
117
            validate_not_part_of_system_pack(action)
118
            action_validator.validate_action(action)
119
        except (ValidationError, ValueError,
120
                ValueValidationException, InvalidActionParameterException) as e:
121
            LOG.exception('Unable to create action data=%s', action)
122
            abort(http_client.BAD_REQUEST, str(e))
123
            return
124
125
        # Write pack data files to disk (if any are provided)
126
        data_files = getattr(action, 'data_files', [])
127
        written_data_files = []
128
        if data_files:
129
            written_data_files = self._handle_data_files(pack_ref=action.pack,
130
                                                         data_files=data_files)
131
132
        action_model = ActionAPI.to_model(action)
133
134
        LOG.debug('/actions/ POST verified ActionAPI object=%s', action)
135
        action_db = Action.add_or_update(action_model)
136
        LOG.debug('/actions/ POST saved ActionDB object=%s', action_db)
137
138
        # Dispatch an internal trigger for each written data file. This way user
139
        # automate comitting this files to git using StackStorm rule
140
        if written_data_files:
141
            self._dispatch_trigger_for_written_data_files(action_db=action_db,
142
                                                          written_data_files=written_data_files)
143
144
        extra = {'acion_db': action_db}
145
        LOG.audit('Action created. Action.id=%s' % (action_db.id), extra=extra)
146
        action_api = ActionAPI.from_model(action_db)
147
148
        return Response(json=action_api, status=http_client.CREATED)
149
150
    def put(self, action, ref_or_id, requester_user):
151
        action_db = self._get_by_ref_or_id(ref_or_id=ref_or_id)
152
153
        # Assert permissions
154
        permission_type = PermissionType.ACTION_MODIFY
155
        rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user,
156
                                                          resource_db=action_db,
157
                                                          permission_type=permission_type)
158
159
        action_id = action_db.id
160
161
        if not getattr(action, 'pack', None):
162
            action.pack = action_db.pack
163
164
        # Perform validation
165
        validate_not_part_of_system_pack(action)
166
        action_validator.validate_action(action)
167
168
        # Write pack data files to disk (if any are provided)
169
        data_files = getattr(action, 'data_files', [])
170
        written_data_files = []
171
        if data_files:
172
            written_data_files = self._handle_data_files(pack_ref=action.pack,
173
                                                         data_files=data_files)
174
175
        try:
176
            action_db = ActionAPI.to_model(action)
177
            LOG.debug('/actions/ PUT incoming action: %s', action_db)
178
            action_db.id = action_id
179
            action_db = Action.add_or_update(action_db)
180
            LOG.debug('/actions/ PUT after add_or_update: %s', action_db)
181
        except (ValidationError, ValueError) as e:
182
            LOG.exception('Unable to update action data=%s', action)
183
            abort(http_client.BAD_REQUEST, str(e))
184
            return
185
186
        # Dispatch an internal trigger for each written data file. This way user
187
        # automate committing this files to git using StackStorm rule
188
        if written_data_files:
189
            self._dispatch_trigger_for_written_data_files(action_db=action_db,
190
                                                          written_data_files=written_data_files)
191
192
        action_api = ActionAPI.from_model(action_db)
193
        LOG.debug('PUT /actions/ client_result=%s', action_api)
194
195
        return action_api
196
197
    def delete(self, ref_or_id, requester_user):
198
        """
199
            Delete an action.
200
201
            Handles requests:
202
                POST /actions/1?_method=delete
203
                DELETE /actions/1
204
                DELETE /actions/mypack.myaction
205
        """
206
        action_db = self._get_by_ref_or_id(ref_or_id=ref_or_id)
207
        action_id = action_db.id
208
209
        permission_type = PermissionType.ACTION_DELETE
210
        rbac_utils.assert_user_has_resource_db_permission(user_db=requester_user,
211
                                                          resource_db=action_db,
212
                                                          permission_type=permission_type)
213
214
        try:
215
            validate_not_part_of_system_pack(action_db)
216
        except ValueValidationException as e:
217
            abort(http_client.BAD_REQUEST, str(e))
218
219
        LOG.debug('DELETE /actions/ lookup with ref_or_id=%s found object: %s',
220
                  ref_or_id, action_db)
221
222
        try:
223
            Action.delete(action_db)
224
        except Exception as e:
225
            LOG.error('Database delete encountered exception during delete of id="%s". '
226
                      'Exception was %s', action_id, e)
227
            abort(http_client.INTERNAL_SERVER_ERROR, str(e))
228
            return
229
230
        extra = {'action_db': action_db}
231
        LOG.audit('Action deleted. Action.id=%s' % (action_db.id), extra=extra)
232
        return Response(status=http_client.NO_CONTENT)
233
234
    def _handle_data_files(self, pack_ref, data_files):
235
        """
236
        Method for handling action data files.
237
238
        This method performs two tasks:
239
240
        1. Writes files to disk
241
        2. Updates affected PackDB model
242
        """
243
        # Write files to disk
244
        written_file_paths = self._write_data_files_to_disk(pack_ref=pack_ref,
245
                                                            data_files=data_files)
246
247
        # Update affected PackDB model (update a list of files)
248
        # Update PackDB
249
        self._update_pack_model(pack_ref=pack_ref, data_files=data_files,
250
                                written_file_paths=written_file_paths)
251
252
        return written_file_paths
253
254
    def _write_data_files_to_disk(self, pack_ref, data_files):
255
        """
256
        Write files to disk.
257
        """
258
        written_file_paths = []
259
260
        for data_file in data_files:
261
            file_path = data_file['file_path']
262
            content = data_file['content']
263
264
            file_path = get_pack_resource_file_abs_path(pack_ref=pack_ref,
265
                                                        resource_type='action',
266
                                                        file_path=file_path)
267
268
            LOG.debug('Writing data file "%s" to "%s"' % (str(data_file), file_path))
269
            self._write_data_file(pack_ref=pack_ref, file_path=file_path, content=content)
270
            written_file_paths.append(file_path)
271
272
        return written_file_paths
273
274
    def _update_pack_model(self, pack_ref, data_files, written_file_paths):
275
        """
276
        Update PackDB models (update files list).
277
        """
278
        file_paths = []  # A list of paths relative to the pack directory for new files
279
        for file_path in written_file_paths:
280
            file_path = get_relative_path_to_pack(pack_ref=pack_ref, file_path=file_path)
281
            file_paths.append(file_path)
282
283
        pack_db = Pack.get_by_ref(pack_ref)
284
        pack_db.files = set(pack_db.files)
285
        pack_db.files.update(set(file_paths))
286
        pack_db.files = list(pack_db.files)
287
        pack_db = Pack.add_or_update(pack_db)
288
289
        return pack_db
290
291
    def _write_data_file(self, pack_ref, file_path, content):
292
        """
293
        Write data file on disk.
294
        """
295
        # Throw if pack directory doesn't exist
296
        pack_base_path = get_pack_base_path(pack_name=pack_ref)
297
        if not os.path.isdir(pack_base_path):
298
            raise ValueError('Directory for pack "%s" doesn\'t exist' % (pack_ref))
299
300
        # Create pack sub-directory tree if it doesn't exist
301
        directory = os.path.dirname(file_path)
302
303
        if not os.path.isdir(directory):
304
            os.makedirs(directory)
305
306
        with open(file_path, 'w') as fp:
307
            fp.write(content)
308
309
    def _dispatch_trigger_for_written_data_files(self, action_db, written_data_files):
310
        trigger = ACTION_FILE_WRITTEN_TRIGGER['name']
311
        host_info = get_host_info()
312
313
        for file_path in written_data_files:
314
            payload = {
315
                'ref': action_db.ref,
316
                'file_path': file_path,
317
                'host_info': host_info
318
            }
319
            self._trigger_dispatcher.dispatch(trigger=trigger, payload=payload)
320
321
322
actions_controller = ActionsController()
323