mirror of
https://github.com/ansible/awx.git
synced 2026-02-04 02:58:13 -03:30
Support AWX_TASK_ENV injection in task and notification invocations.
This change _only_ injects `AWS_TASK_ENV` into `os.environ`; it's up to underlying libraries to be good citizens and actually respect things like `HTTPS_PROXY`. see: #3508
This commit is contained in:
@@ -210,6 +210,17 @@ register(
|
|||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
register(
|
||||||
|
'AWX_TASK_ENV',
|
||||||
|
field_class=fields.DictField,
|
||||||
|
default={},
|
||||||
|
label=_('Extra Environment Variables'),
|
||||||
|
help_text=_('Additional environment variables set for playbook runs, inventory updates, project updates, and notification sending.'),
|
||||||
|
category=_('Jobs'),
|
||||||
|
category_slug='jobs',
|
||||||
|
placeholder={'HTTP_PROXY': 'myproxy.local:8080'},
|
||||||
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
'STDOUT_MAX_BYTES_DISPLAY',
|
'STDOUT_MAX_BYTES_DISPLAY',
|
||||||
field_class=fields.IntegerField,
|
field_class=fields.IntegerField,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
from django.core.mail.message import EmailMessage
|
from django.core.mail.message import EmailMessage
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_str, force_text
|
from django.utils.encoding import smart_str, force_text
|
||||||
@@ -11,7 +12,7 @@ from django.utils.encoding import smart_str, force_text
|
|||||||
# AWX
|
# AWX
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.models.base import * # noqa
|
from awx.main.models.base import * # noqa
|
||||||
from awx.main.utils import encrypt_field, decrypt_field
|
from awx.main.utils import encrypt_field, decrypt_field, set_environ
|
||||||
from awx.main.notifications.email_backend import CustomEmailBackend
|
from awx.main.notifications.email_backend import CustomEmailBackend
|
||||||
from awx.main.notifications.slack_backend import SlackBackend
|
from awx.main.notifications.slack_backend import SlackBackend
|
||||||
from awx.main.notifications.twilio_backend import TwilioBackend
|
from awx.main.notifications.twilio_backend import TwilioBackend
|
||||||
@@ -117,7 +118,8 @@ class NotificationTemplate(CommonModelNameNotUnique):
|
|||||||
sender = self.notification_configuration.pop(self.notification_class.sender_parameter, None)
|
sender = self.notification_configuration.pop(self.notification_class.sender_parameter, None)
|
||||||
backend_obj = self.notification_class(**self.notification_configuration)
|
backend_obj = self.notification_class(**self.notification_configuration)
|
||||||
notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients)
|
notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients)
|
||||||
return backend_obj.send_messages([notification_obj])
|
with set_environ(**settings.AWX_TASK_ENV):
|
||||||
|
return backend_obj.send_messages([notification_obj])
|
||||||
|
|
||||||
def display_notification_configuration(self):
|
def display_notification_configuration(self):
|
||||||
field_val = self.notification_configuration.copy()
|
field_val = self.notification_configuration.copy()
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from requests.utils import select_proxy
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.models.notifications import NotificationTemplate, Notification
|
from awx.main.models.notifications import NotificationTemplate, Notification
|
||||||
from awx.main.models.inventory import Inventory, InventorySource
|
from awx.main.models.inventory import Inventory, InventorySource
|
||||||
@@ -129,3 +133,26 @@ def test_disallow_delete_when_notifications_pending(delete, user, notification_t
|
|||||||
response = delete(url, user=u)
|
response = delete(url, user=u)
|
||||||
assert response.status_code == 405
|
assert response.status_code == 405
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_custom_environment_injection(post, user, organization):
|
||||||
|
u = user('admin-poster', True)
|
||||||
|
url = reverse('api:notification_template_list')
|
||||||
|
response = post(url,
|
||||||
|
dict(name="test-webhook",
|
||||||
|
description="test webhook",
|
||||||
|
organization=organization.id,
|
||||||
|
notification_type="webhook",
|
||||||
|
notification_configuration=dict(url="https://example.org",
|
||||||
|
headers={"Test": "Header"})),
|
||||||
|
u)
|
||||||
|
assert response.status_code == 201
|
||||||
|
template = NotificationTemplate.objects.get(pk=response.data['id'])
|
||||||
|
with pytest.raises(ConnectionError), \
|
||||||
|
mock.patch('django.conf.settings.AWX_TASK_ENV', {'HTTPS_PROXY': '192.168.50.100:1234'}), \
|
||||||
|
mock.patch.object(HTTPAdapter, 'send') as fake_send:
|
||||||
|
def _send_side_effect(request, **kw):
|
||||||
|
assert select_proxy(request.url, kw['proxies']) == '192.168.50.100:1234'
|
||||||
|
raise ConnectionError()
|
||||||
|
fake_send.side_effect = _send_side_effect
|
||||||
|
template.send('subject', 'message')
|
||||||
|
|||||||
@@ -290,6 +290,17 @@ class TestGenericRun(TestJobExecution):
|
|||||||
args, cwd, env, stdout = call_args
|
args, cwd, env, stdout = call_args
|
||||||
assert args[0] == 'bwrap'
|
assert args[0] == 'bwrap'
|
||||||
|
|
||||||
|
def test_awx_task_env(self):
|
||||||
|
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
|
||||||
|
patch.start()
|
||||||
|
|
||||||
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
assert self.run_pexpect.call_count == 1
|
||||||
|
call_args, _ = self.run_pexpect.call_args_list[0]
|
||||||
|
args, cwd, env, stdout = call_args
|
||||||
|
assert env['FOO'] == 'BAR'
|
||||||
|
|
||||||
|
|
||||||
class TestIsolatedExecution(TestJobExecution):
|
class TestIsolatedExecution(TestJobExecution):
|
||||||
|
|
||||||
@@ -1035,6 +1046,17 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
self.run_pexpect.side_effect = run_pexpect_side_effect
|
self.run_pexpect.side_effect = run_pexpect_side_effect
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
def test_awx_task_env(self):
|
||||||
|
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
|
||||||
|
patch.start()
|
||||||
|
|
||||||
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
assert self.run_pexpect.call_count == 1
|
||||||
|
call_args, _ = self.run_pexpect.call_args_list[0]
|
||||||
|
args, cwd, env, stdout = call_args
|
||||||
|
assert env['FOO'] == 'BAR'
|
||||||
|
|
||||||
|
|
||||||
class TestProjectUpdateCredentials(TestJobExecution):
|
class TestProjectUpdateCredentials(TestJobExecution):
|
||||||
|
|
||||||
@@ -1056,6 +1078,11 @@ class TestProjectUpdateCredentials(TestJobExecution):
|
|||||||
dict(scm_type='git'),
|
dict(scm_type='git'),
|
||||||
dict(scm_type='hg'),
|
dict(scm_type='hg'),
|
||||||
dict(scm_type='svn'),
|
dict(scm_type='svn'),
|
||||||
|
],
|
||||||
|
'test_awx_task_env': [
|
||||||
|
dict(scm_type='git'),
|
||||||
|
dict(scm_type='hg'),
|
||||||
|
dict(scm_type='svn'),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,6 +1140,18 @@ class TestProjectUpdateCredentials(TestJobExecution):
|
|||||||
self.run_pexpect.side_effect = partial(run_pexpect_side_effect, private_data)
|
self.run_pexpect.side_effect = partial(run_pexpect_side_effect, private_data)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
def test_awx_task_env(self, scm_type):
|
||||||
|
self.instance.scm_type = scm_type
|
||||||
|
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
|
||||||
|
patch.start()
|
||||||
|
|
||||||
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
assert self.run_pexpect.call_count == 1
|
||||||
|
call_args, _ = self.run_pexpect.call_args_list[0]
|
||||||
|
args, cwd, env, stdout = call_args
|
||||||
|
assert env['FOO'] == 'BAR'
|
||||||
|
|
||||||
|
|
||||||
class TestInventoryUpdateCredentials(TestJobExecution):
|
class TestInventoryUpdateCredentials(TestJobExecution):
|
||||||
|
|
||||||
@@ -1323,6 +1362,28 @@ class TestInventoryUpdateCredentials(TestJobExecution):
|
|||||||
self.run_pexpect.side_effect = run_pexpect_side_effect
|
self.run_pexpect.side_effect = run_pexpect_side_effect
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
def test_awx_task_env(self):
|
||||||
|
gce = CredentialType.defaults['gce']()
|
||||||
|
self.instance.source = 'gce'
|
||||||
|
self.instance.credential = Credential(
|
||||||
|
pk=1,
|
||||||
|
credential_type=gce,
|
||||||
|
inputs = {
|
||||||
|
'username': 'bob',
|
||||||
|
'project': 'some-project',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
|
||||||
|
patch.start()
|
||||||
|
|
||||||
|
self.task.run(self.pk)
|
||||||
|
|
||||||
|
assert self.run_pexpect.call_count == 1
|
||||||
|
call_args, _ = self.run_pexpect.call_args_list[0]
|
||||||
|
args, cwd, env, stdout = call_args
|
||||||
|
assert env['FOO'] == 'BAR'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_os_open_oserror():
|
def test_os_open_oserror():
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
# Copyright (c) 2017 Ansible, Inc.
|
# Copyright (c) 2017 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from awx.main.utils import common
|
from awx.main.utils import common
|
||||||
|
|
||||||
@@ -15,3 +17,13 @@ from awx.main.utils import common
|
|||||||
])
|
])
|
||||||
def test_parse_yaml_or_json(input_, output):
|
def test_parse_yaml_or_json(input_, output):
|
||||||
assert common.parse_yaml_or_json(input_) == output
|
assert common.parse_yaml_or_json(input_) == output
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_environ():
|
||||||
|
key = str(uuid4())
|
||||||
|
old_environ = os.environ.copy()
|
||||||
|
with common.set_environ(**{key: 'bar'}):
|
||||||
|
assert os.environ[key] == 'bar'
|
||||||
|
assert set(os.environ.keys()) - set(old_environ.keys()) == set([key])
|
||||||
|
assert os.environ == old_environ
|
||||||
|
assert key not in os.environ
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
|
|||||||
'callback_filter_out_ansible_extra_vars', 'get_search_fields', 'get_system_task_capacity',
|
'callback_filter_out_ansible_extra_vars', 'get_search_fields', 'get_system_task_capacity',
|
||||||
'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict',
|
'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict',
|
||||||
'model_instance_diff', 'timestamp_apiformat', 'parse_yaml_or_json', 'RequireDebugTrueOrTest',
|
'model_instance_diff', 'timestamp_apiformat', 'parse_yaml_or_json', 'RequireDebugTrueOrTest',
|
||||||
'has_model_field_prefetched']
|
'has_model_field_prefetched', 'set_environ']
|
||||||
|
|
||||||
|
|
||||||
def get_object_or_400(klass, *args, **kwargs):
|
def get_object_or_400(klass, *args, **kwargs):
|
||||||
@@ -583,6 +583,23 @@ def ignore_inventory_group_removal():
|
|||||||
_inventory_updates.is_removing = previous_value
|
_inventory_updates.is_removing = previous_value
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def set_environ(**environ):
|
||||||
|
'''
|
||||||
|
Temporarily set the process environment variables.
|
||||||
|
|
||||||
|
>>> with set_environ(FOO='BAR'):
|
||||||
|
... assert os.environ['FOO'] == 'BAR'
|
||||||
|
'''
|
||||||
|
old_environ = os.environ.copy()
|
||||||
|
try:
|
||||||
|
os.environ.update(environ)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
os.environ.clear()
|
||||||
|
os.environ.update(old_environ)
|
||||||
|
|
||||||
|
|
||||||
@memoize()
|
@memoize()
|
||||||
def check_proot_installed():
|
def check_proot_installed():
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -37,3 +37,6 @@
|
|||||||
* Tower now uses a modified version of [Fernet](https://github.com/fernet/spec/blob/master/Spec.md).
|
* Tower now uses a modified version of [Fernet](https://github.com/fernet/spec/blob/master/Spec.md).
|
||||||
Our `Fernet256` class uses `AES-256-CBC` instead of `AES-128-CBC` for all encrypted fields.
|
Our `Fernet256` class uses `AES-256-CBC` instead of `AES-128-CBC` for all encrypted fields.
|
||||||
[[#826](https://github.com/ansible/ansible-tower/issues/826)]
|
[[#826](https://github.com/ansible/ansible-tower/issues/826)]
|
||||||
|
* Added the ability to set custom environment variables set for playbook runs,
|
||||||
|
inventory updates, project updates, and notification sending.
|
||||||
|
[[#3508](https://github.com/ansible/ansible-tower/issues/3508)]
|
||||||
|
|||||||
Reference in New Issue
Block a user