Skip to content

Commit

Permalink
Merge pull request #2229 from StackStorm/chatops/ack
Browse files Browse the repository at this point in the history
Ack message formatting in AliasExecution
  • Loading branch information
emedvedev committed Nov 24, 2015
2 parents 9f0a5fa + 68e81ea commit ca638c9
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 30 deletions.
20 changes: 16 additions & 4 deletions st2api/st2api/controllers/v1/aliasexecution.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
import pecan
import six
from pecan import rest

from st2common import log as logging
from st2common.models.api.base import jsexpose
from st2common.exceptions.db import StackStormDBObjectNotFoundError
from st2common.models.api.action import AliasExecutionAPI
from st2common.models.api.action import ActionAliasAPI
from st2common.models.api.auth import get_system_username
from st2common.models.api.execution import ActionExecutionAPI
from st2common.models.db.liveaction import LiveActionDB
from st2common.models.db.notification import NotificationSchema, NotificationSubSchema
from st2common.models.utils import action_alias_utils, action_param_utils
Expand All @@ -31,6 +32,7 @@
from st2common.util import action_db as action_utils
from st2common.util import reference
from st2common.util.api import get_requester
from st2common.util.jinja import render_values as render
from st2common.rbac.types import PermissionType
from st2common.rbac.utils import assert_request_user_has_resource_db_permission

Expand All @@ -46,7 +48,7 @@

class ActionAliasExecutionController(rest.RestController):

@jsexpose(body_cls=AliasExecutionAPI, status_code=http_client.OK)
@jsexpose(body_cls=AliasExecutionAPI, status_code=http_client.CREATED)
def post(self, payload):
action_alias_name = payload.name if payload else None

Expand Down Expand Up @@ -88,7 +90,17 @@ def post(self, payload):
notify=notify,
context=context)

return str(execution.id)
result = {
'execution': execution,
'actionalias': ActionAliasAPI.from_model(action_alias_db)
}

if action_alias_db.ack and 'format' in action_alias_db.ack:
result.update({
'message': render({'alias': action_alias_db.ack['format']}, result)['alias']
})

return result

def _tokenize_alias_execution(self, alias_execution):
tokens = alias_execution.strip().split(' ', 1)
Expand Down Expand Up @@ -140,7 +152,7 @@ def _schedule_execution(self, action_alias_db, params, notify, context):
liveaction = LiveActionDB(action=action_alias_db.action_ref, context=context,
parameters=params, notify=notify)
_, action_execution_db = action_service.request(liveaction)
return action_execution_db
return ActionExecutionAPI.from_model(action_execution_db)
except ValueError as e:
LOG.exception('Unable to execute action.')
pecan.abort(http_client.BAD_REQUEST, str(e))
Expand Down
24 changes: 11 additions & 13 deletions st2api/tests/unit/controllers/v1/test_alias_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import mock

from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED
from st2common.models.db.execution import ActionExecutionDB
from st2common.services import action as action_service
from st2tests.fixturesloader import FixturesLoader
from tests import FunctionalTest
Expand All @@ -32,18 +33,15 @@
'aliases': ['alias3.yaml']
}

EXECUTION = ActionExecutionDB(id='54e657d60640fd16887d6855',
status=LIVEACTION_STATUS_SUCCEEDED,
result='')

__all__ = [
'AliasExecutionTestCase'
]


class DummyActionExecution(object):
def __init__(self, id_=None, status=LIVEACTION_STATUS_SUCCEEDED, result=''):
self.id = id_
self.status = status
self.result = result


class AliasExecutionTestCase(FunctionalTest):

models = None
Expand All @@ -59,29 +57,29 @@ def setUpClass(cls):
cls.alias2 = cls.models['aliases']['alias2.yaml']

@mock.patch.object(action_service, 'request',
return_value=(None, DummyActionExecution(id_=1)))
return_value=(None, EXECUTION))
def test_basic_execution(self, request):
command = 'Lorem ipsum value1 dolor sit "value2 value3" amet.'
post_resp = self._do_post(alias_execution=self.alias1, command=command)
self.assertEqual(post_resp.status_int, 200)
self.assertEqual(post_resp.status_int, 201)
expected_parameters = {'param1': 'value1', 'param2': 'value2 value3'}
self.assertEquals(request.call_args[0][0].parameters, expected_parameters)

@mock.patch.object(action_service, 'request',
return_value=(None, DummyActionExecution(id_=1)))
return_value=(None, EXECUTION))
def test_execution_with_array_type_single_value(self, request):
command = 'Lorem ipsum value1 dolor sit value2 amet.'
post_resp = self._do_post(alias_execution=self.alias2, command=command)
self.assertEqual(post_resp.status_int, 200)
self.assertEqual(post_resp.status_int, 201)
expected_parameters = {'param1': 'value1', 'param3': ['value2']}
self.assertEquals(request.call_args[0][0].parameters, expected_parameters)

@mock.patch.object(action_service, 'request',
return_value=(None, DummyActionExecution(id_=1)))
return_value=(None, EXECUTION))
def test_execution_with_array_type_multi_value(self, request):
command = 'Lorem ipsum value1 dolor sit "value2, value3" amet.'
post_resp = self._do_post(alias_execution=self.alias2, command=command)
self.assertEqual(post_resp.status_int, 200)
self.assertEqual(post_resp.status_int, 201)
expected_parameters = {'param1': 'value1', 'param3': ['value2', 'value3']}
self.assertEquals(request.call_args[0][0].parameters, expected_parameters)

Expand Down
13 changes: 9 additions & 4 deletions st2api/tests/unit/controllers/v1/test_alias_execution_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@

import mock

from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED
from st2common.models.db.execution import ActionExecutionDB
from st2common.services import action as action_service
from st2tests.fixturesloader import FixturesLoader
from tests.base import APIControllerWithRBACTestCase
from tests.unit.controllers.v1.test_alias_execution import DummyActionExecution

FIXTURES_PACK = 'aliases'

Expand All @@ -32,6 +33,10 @@
'aliases': ['alias3.yaml']
}

EXECUTION = ActionExecutionDB(id='54e657d60640fd16887d6855',
status=LIVEACTION_STATUS_SUCCEEDED,
result='')

__all__ = [
'AliasExecutionWithRBACTestCase'
]
Expand All @@ -43,12 +48,12 @@ def setUp(self):
super(AliasExecutionWithRBACTestCase, self).setUp()

self.models = FixturesLoader().save_fixtures_to_db(fixtures_pack=FIXTURES_PACK,
fixtures_dict=TEST_MODELS)
fixtures_dict=TEST_MODELS)
self.alias1 = self.models['aliases']['alias1.yaml']
self.alias2 = self.models['aliases']['alias2.yaml']

@mock.patch.object(action_service, 'request',
return_value=(None, DummyActionExecution(id_=1)))
return_value=(None, EXECUTION))
def test_live_action_context_user_is_set_to_authenticated_user(self, request):
# Verify that the user inside the context of live action is set to authenticated user
# which hit the endpoint. This is important for RBAC and many other things.
Expand All @@ -57,7 +62,7 @@ def test_live_action_context_user_is_set_to_authenticated_user(self, request):

command = 'Lorem ipsum value1 dolor sit "value2, value3" amet.'
post_resp = self._do_post(alias_execution=self.alias2, command=command)
self.assertEqual(post_resp.status_int, 200)
self.assertEqual(post_resp.status_int, 201)

live_action_db = request.call_args[0][0]
self.assertEquals(live_action_db.context['user'], 'admin')
Expand Down
26 changes: 18 additions & 8 deletions st2common/st2common/models/utils/action_alias_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ def get_extracted_param_value(self):
if extra:
kv_pairs = re.findall(pairs_match,
extra.group(1), re.DOTALL)
for pair in kv_pairs:
result[pair[0]] = ''.join(pair[2:])
self._param_stream = self._param_stream.replace(extra.group(1), '')
self._param_stream = " %s " % self._param_stream

# Now we'll match parameters with default values in form of
# {{ value = parameter }} (and all possible permutations of spaces),
Expand All @@ -59,25 +58,36 @@ def get_extracted_param_value(self):
# Now we're transforming our format string into a regular expression,
# substituting {{ ... }} with regex named groups, so that param_stream
# matched against this expression yields a dict of params with values.
reg = re.sub(r'\s+{{\s*(\S+)\s*=(?:{.+?}|.+?)}}',
r'(?:\s+[\'"]?(?P<\1>.+?)[\'"]?)?',
param_match = r'["\']?(?P<\2>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']?'
reg = re.sub(r'(\s*){{\s*([^=]+?)\s*}}(?=\s+{{[^}]+?=)',
r'\s*' + param_match + r'\s+',
self._format)
reg = re.sub(r'{{\s*(.+?)\s*}}',
r'[\'"]?(?P<\1>.+?)[\'"]?',
reg = re.sub(r'(\s*){{\s*(\S+)\s*=\s*(?:{.+?}|.+?)\s*}}(\s*)',
r'(?:\s*' + param_match + r'\s+)?\s*',
reg)
reg = re.sub(r'(\s*){{\s*(.+?)\s*}}(\s*)',
r'\s*' + param_match + r'\3',
reg)
reg = reg + r'\s*$'
reg = '^\s*' + reg + r'\s*$'

# Now we're matching param_stream against our format string regex,
# getting a dict of values. We'll also get default values from
# "params" list if something is not present.
# Priority, from lowest to highest:
# 1. Default parameters
# 2. Matched parameters
# 3. Extra parameters
matched_stream = re.match(reg, self._param_stream, re.DOTALL)
if matched_stream:
values = matched_stream.groupdict()
for param in params:
matched_value = values[param[0]] if matched_stream else None
result[param[0]] = matched_value or param[1]
if extra:
for pair in kv_pairs:
result[pair[0]] = ''.join(pair[2:])

if self._format and not (self._param_stream or any(result.values())):
if self._format and not (self._param_stream.strip() or any(result.values())):
raise content.ParseException('No value supplied and no default value found.')

return result
5 changes: 4 additions & 1 deletion st2common/st2common/util/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@ def get_jinja_environment(allow_undefined=False):
'''
undefined = jinja2.Undefined if allow_undefined else jinja2.StrictUndefined
env = jinja2.Environment(undefined=undefined)
env = jinja2.Environment(undefined=undefined,
trim_blocks=True,
lstrip_blocks=True)
env.filters.update(CustomFilters.get_filters())
env.tests['in'] = lambda item, list: item in list
return env


Expand Down
13 changes: 13 additions & 0 deletions st2common/tests/unit/test_action_alias_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,16 @@ def test_stream_is_none_no_default_values(self):
expected_msg = 'No value supplied and no default value found.'
self.assertRaisesRegexp(ParseException, expected_msg,
parser.get_extracted_param_value)

def test_all_the_things(self):
# this is the most insane example I could come up with
alias_format = "{{ p0='http' }} g {{ p1=p }} a " + \
"{{ url }} {{ p2={'a':'b'} }} {{ p3={{ e.i }} }}"
param_stream = "g a http://google.com {{ execution.id }} p4='testing' p5={'a':'c'}"
parser = ActionAliasFormatParser(alias_format, param_stream)
extracted_values = parser.get_extracted_param_value()
self.assertEqual(extracted_values, {'p0': 'http', 'p1': 'p',
'url': 'http://google.com',
'p2': '{{ execution.id }}',
'p3': '{{ e.i }}',
'p4': 'testing', 'p5': "{'a':'c'}"})

0 comments on commit ca638c9

Please sign in to comment.