mirror of
https://github.com/ansible/awx.git
synced 2026-05-14 12:57:40 -02:30
Merge branch 'stable' into devel
* stable: (30 commits) Remove ansible-tower.repo during packer cleanup Update package changelogs Enable SE boolean httpd_execmem Check that venv is activated before loading wsgi app (#3148) also limit creation of system auditors to superusers Make check for idle session more specific Use foreman.id instead flake8 fixups fixing tests for new team role assignment restrictions driveby cleanup do not allow assignment of system roles or user.admin_role to teams Add copyright back with utf-8 encoding Complete Sat6 integration Force requests to emit application/json fix issue with rbac_migrations logger Enable software collections repo on RHEL 6 + RHSM Fixed credential migration issue involving null inventory fields in job templates Allow instant cancel for new jobs Show new jobs in UI Jobs tab use existing logging infrastructure ...
This commit is contained in:
@@ -1573,6 +1573,7 @@ class InventoryScanJobTemplateList(SubListAPIView):
|
|||||||
|
|
||||||
class HostList(ListCreateAPIView):
|
class HostList(ListCreateAPIView):
|
||||||
|
|
||||||
|
always_allow_superuser = False
|
||||||
model = Host
|
model = Host
|
||||||
serializer_class = HostSerializer
|
serializer_class = HostSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -245,16 +245,18 @@ class UserAccess(BaseAccess):
|
|||||||
|
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
if data is not None and 'is_superuser' in data:
|
if data is not None and ('is_superuser' in data or 'is_system_auditor' in data):
|
||||||
if to_python_boolean(data['is_superuser'], allow_none=True) and not self.user.is_superuser:
|
if (to_python_boolean(data.get('is_superuser', 'false'), allow_none=True) or
|
||||||
|
to_python_boolean(data.get('is_system_auditor', 'false'), allow_none=True)) and not self.user.is_superuser:
|
||||||
return False
|
return False
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
return True
|
return True
|
||||||
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
return Organization.accessible_objects(self.user, 'admin_role').exists()
|
||||||
|
|
||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
if data is not None and 'is_superuser' in data:
|
if data is not None and ('is_superuser' in data or 'is_system_auditor' in data):
|
||||||
if to_python_boolean(data['is_superuser'], allow_none=True) and not self.user.is_superuser:
|
if (to_python_boolean(data.get('is_superuser', 'false'), allow_none=True) or
|
||||||
|
to_python_boolean(data.get('is_system_auditor', 'false'), allow_none=True)) and not self.user.is_superuser:
|
||||||
return False
|
return False
|
||||||
# A user can be changed if they are themselves, or by org admins or
|
# A user can be changed if they are themselves, or by org admins or
|
||||||
# superusers. Change permission implies changing only certain fields
|
# superusers. Change permission implies changing only certain fields
|
||||||
@@ -720,18 +722,25 @@ class TeamAccess(BaseAccess):
|
|||||||
def can_attach(self, obj, sub_obj, relationship, *args, **kwargs):
|
def can_attach(self, obj, sub_obj, relationship, *args, **kwargs):
|
||||||
"""Reverse obj and sub_obj, defer to RoleAccess if this is an assignment
|
"""Reverse obj and sub_obj, defer to RoleAccess if this is an assignment
|
||||||
of a resource role to the team."""
|
of a resource role to the team."""
|
||||||
if isinstance(sub_obj, Role) and isinstance(sub_obj.content_object, ResourceMixin):
|
if isinstance(sub_obj, Role):
|
||||||
role_access = RoleAccess(self.user)
|
if sub_obj.content_object is None:
|
||||||
return role_access.can_attach(sub_obj, obj, 'member_role.parents',
|
raise PermissionDenied("The {} role cannot be assigned to a team".format(sub_obj.name))
|
||||||
*args, **kwargs)
|
elif isinstance(sub_obj.content_object, User):
|
||||||
|
raise PermissionDenied("The admin_role for a User cannot be assigned to a team")
|
||||||
|
|
||||||
|
if isinstance(sub_obj.content_object, ResourceMixin):
|
||||||
|
role_access = RoleAccess(self.user)
|
||||||
|
return role_access.can_attach(sub_obj, obj, 'member_role.parents',
|
||||||
|
*args, **kwargs)
|
||||||
return super(TeamAccess, self).can_attach(obj, sub_obj, relationship,
|
return super(TeamAccess, self).can_attach(obj, sub_obj, relationship,
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
|
|
||||||
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
||||||
if isinstance(sub_obj, Role) and isinstance(sub_obj.content_object, ResourceMixin):
|
if isinstance(sub_obj, Role):
|
||||||
role_access = RoleAccess(self.user)
|
if isinstance(sub_obj.content_object, ResourceMixin):
|
||||||
return role_access.can_unattach(sub_obj, obj, 'member_role.parents',
|
role_access = RoleAccess(self.user)
|
||||||
*args, **kwargs)
|
return role_access.can_unattach(sub_obj, obj, 'member_role.parents',
|
||||||
|
*args, **kwargs)
|
||||||
return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship,
|
return super(TeamAccess, self).can_unattach(obj, sub_obj, relationship,
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
|
|
||||||
@@ -906,8 +915,7 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
project = get_value(Project, 'project')
|
project = get_value(Project, 'project')
|
||||||
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
|
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
|
||||||
if inventory:
|
if inventory:
|
||||||
org = inventory.organization
|
accessible = self.user in inventory.use_role
|
||||||
accessible = self.user in org.admin_role
|
|
||||||
else:
|
else:
|
||||||
accessible = False
|
accessible = False
|
||||||
if not project and accessible:
|
if not project and accessible:
|
||||||
@@ -979,7 +987,8 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
|
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
if hasattr(obj, k) and getattr(obj, k) != v:
|
if hasattr(obj, k) and getattr(obj, k) != v:
|
||||||
if k not in field_whitelist and v != getattr(obj, '%s_id' % k, None):
|
if k not in field_whitelist and v != getattr(obj, '%s_id' % k, None) \
|
||||||
|
and not (hasattr(obj, '%s_id' % k) and getattr(obj, '%s_id' % k) is None and v == ''): # Equate '' to None in the case of foreign keys
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1681,8 +1690,7 @@ class RoleAccess(BaseAccess):
|
|||||||
if not check_user_access(self.user, sub_obj.__class__, 'read', sub_obj):
|
if not check_user_access(self.user, sub_obj.__class__, 'read', sub_obj):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if obj.object_id and \
|
if isinstance(obj.content_object, ResourceMixin) and \
|
||||||
isinstance(obj.content_object, ResourceMixin) and \
|
|
||||||
self.user in obj.content_object.admin_role:
|
self.user in obj.content_object.admin_role:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -481,6 +481,7 @@ def load_inventory_source(source, all_group=None, group_filter_re=None,
|
|||||||
# Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
|
# Sanity check: We sanitize these module names for our API but Ansible proper doesn't follow
|
||||||
# good naming conventions
|
# good naming conventions
|
||||||
source = source.replace('azure.py', 'windows_azure.py')
|
source = source.replace('azure.py', 'windows_azure.py')
|
||||||
|
source = source.replace('satellite6.py', 'foreman.py')
|
||||||
logger.debug('Analyzing type of source: %s', source)
|
logger.debug('Analyzing type of source: %s', source)
|
||||||
original_all_group = all_group
|
original_all_group = all_group
|
||||||
if not os.path.exists(source):
|
if not os.path.exists(source):
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ from __future__ import unicode_literals
|
|||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from awx.api.license import feature_enabled
|
|
||||||
|
|
||||||
|
|
||||||
def create_system_job_templates(apps, schema_editor):
|
def create_system_job_templates(apps, schema_editor):
|
||||||
'''
|
'''
|
||||||
Create default system job templates if not present. Create default schedules
|
Create default system job templates if not present. Create default schedules
|
||||||
@@ -80,7 +77,7 @@ def create_system_job_templates(apps, schema_editor):
|
|||||||
polymorphic_ctype=sjt_ct,
|
polymorphic_ctype=sjt_ct,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if created and feature_enabled('system_tracking', bypass_database=True):
|
if created:
|
||||||
sched = Schedule(
|
sched = Schedule(
|
||||||
name='Cleanup Fact Schedule',
|
name='Cleanup Fact Schedule',
|
||||||
rrule='DTSTART:%s RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1' % now_str,
|
rrule='DTSTART:%s RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1' % now_str,
|
||||||
|
|||||||
@@ -8,25 +8,8 @@ from collections import defaultdict
|
|||||||
from awx.main.utils import getattrd
|
from awx.main.utils import getattrd
|
||||||
from awx.main.models.rbac import Role, batch_role_ancestor_rebuilding
|
from awx.main.models.rbac import Role, batch_role_ancestor_rebuilding
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('rbac_migrations')
|
||||||
|
|
||||||
def log_migration(wrapped):
|
|
||||||
'''setup the logging mechanism for each migration method
|
|
||||||
as it runs, Django resets this, so we use a decorator
|
|
||||||
to re-add the handler for each method.
|
|
||||||
'''
|
|
||||||
handler = logging.FileHandler("/tmp/tower_rbac_migrations.log", mode="a", encoding="UTF-8")
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
handler.setLevel(logging.DEBUG)
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
logger.handlers = []
|
|
||||||
logger.addHandler(handler)
|
|
||||||
return wrapped(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def create_roles(apps, schema_editor):
|
def create_roles(apps, schema_editor):
|
||||||
'''
|
'''
|
||||||
Implicit role creation happens in our post_save hook for all of our
|
Implicit role creation happens in our post_save hook for all of our
|
||||||
@@ -56,7 +39,6 @@ def create_roles(apps, schema_editor):
|
|||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_users(apps, schema_editor):
|
def migrate_users(apps, schema_editor):
|
||||||
User = apps.get_model('auth', "User")
|
User = apps.get_model('auth', "User")
|
||||||
Role = apps.get_model('main', "Role")
|
Role = apps.get_model('main', "Role")
|
||||||
@@ -89,7 +71,6 @@ def migrate_users(apps, schema_editor):
|
|||||||
sa_role.members.add(user)
|
sa_role.members.add(user)
|
||||||
logger.warning(smart_text(u"added superuser: {}".format(user.username)))
|
logger.warning(smart_text(u"added superuser: {}".format(user.username)))
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_organization(apps, schema_editor):
|
def migrate_organization(apps, schema_editor):
|
||||||
Organization = apps.get_model('main', "Organization")
|
Organization = apps.get_model('main', "Organization")
|
||||||
for org in Organization.objects.iterator():
|
for org in Organization.objects.iterator():
|
||||||
@@ -100,7 +81,6 @@ def migrate_organization(apps, schema_editor):
|
|||||||
org.member_role.members.add(user)
|
org.member_role.members.add(user)
|
||||||
logger.info(smart_text(u"added member: {}, {}".format(org.name, user.username)))
|
logger.info(smart_text(u"added member: {}, {}".format(org.name, user.username)))
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_team(apps, schema_editor):
|
def migrate_team(apps, schema_editor):
|
||||||
Team = apps.get_model('main', 'Team')
|
Team = apps.get_model('main', 'Team')
|
||||||
for t in Team.objects.iterator():
|
for t in Team.objects.iterator():
|
||||||
@@ -172,7 +152,6 @@ def _discover_credentials(instances, cred, orgfunc):
|
|||||||
|
|
||||||
_update_credential_parents(org, cred)
|
_update_credential_parents(org, cred)
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_credential(apps, schema_editor):
|
def migrate_credential(apps, schema_editor):
|
||||||
Credential = apps.get_model('main', "Credential")
|
Credential = apps.get_model('main', "Credential")
|
||||||
JobTemplate = apps.get_model('main', 'JobTemplate')
|
JobTemplate = apps.get_model('main', 'JobTemplate')
|
||||||
@@ -180,7 +159,7 @@ def migrate_credential(apps, schema_editor):
|
|||||||
InventorySource = apps.get_model('main', 'InventorySource')
|
InventorySource = apps.get_model('main', 'InventorySource')
|
||||||
|
|
||||||
for cred in Credential.objects.iterator():
|
for cred in Credential.objects.iterator():
|
||||||
results = [x for x in JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred)).all()] + \
|
results = [x for x in JobTemplate.objects.filter(Q(credential=cred) | Q(cloud_credential=cred), inventory__isnull=False).all()] + \
|
||||||
[x for x in InventorySource.objects.filter(credential=cred).all()]
|
[x for x in InventorySource.objects.filter(credential=cred).all()]
|
||||||
if cred.deprecated_team is not None and results:
|
if cred.deprecated_team is not None and results:
|
||||||
if len(results) == 1:
|
if len(results) == 1:
|
||||||
@@ -210,7 +189,6 @@ def migrate_credential(apps, schema_editor):
|
|||||||
logger.warning(smart_text(u"orphaned credential found Credential(name={}, kind={}, host={}), superuser only".format(cred.name, cred.kind, cred.host, )))
|
logger.warning(smart_text(u"orphaned credential found Credential(name={}, kind={}, host={}), superuser only".format(cred.name, cred.kind, cred.host, )))
|
||||||
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_inventory(apps, schema_editor):
|
def migrate_inventory(apps, schema_editor):
|
||||||
Inventory = apps.get_model('main', 'Inventory')
|
Inventory = apps.get_model('main', 'Inventory')
|
||||||
Permission = apps.get_model('main', 'Permission')
|
Permission = apps.get_model('main', 'Permission')
|
||||||
@@ -254,7 +232,6 @@ def migrate_inventory(apps, schema_editor):
|
|||||||
execrole.members.add(perm.user)
|
execrole.members.add(perm.user)
|
||||||
logger.info(smart_text(u'added User({}) access to Inventory({})'.format(perm.user.username, inventory.name)))
|
logger.info(smart_text(u'added User({}) access to Inventory({})'.format(perm.user.username, inventory.name)))
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_projects(apps, schema_editor):
|
def migrate_projects(apps, schema_editor):
|
||||||
'''
|
'''
|
||||||
I can see projects when:
|
I can see projects when:
|
||||||
@@ -368,7 +345,6 @@ def migrate_projects(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_job_templates(apps, schema_editor):
|
def migrate_job_templates(apps, schema_editor):
|
||||||
'''
|
'''
|
||||||
NOTE: This must be run after orgs, inventory, projects, credential, and
|
NOTE: This must be run after orgs, inventory, projects, credential, and
|
||||||
@@ -420,6 +396,11 @@ def migrate_job_templates(apps, schema_editor):
|
|||||||
jt_queryset = JobTemplate.objects.select_related('inventory', 'project', 'inventory__organization', 'execute_role')
|
jt_queryset = JobTemplate.objects.select_related('inventory', 'project', 'inventory__organization', 'execute_role')
|
||||||
|
|
||||||
for jt in jt_queryset.iterator():
|
for jt in jt_queryset.iterator():
|
||||||
|
if jt.inventory is None:
|
||||||
|
# If inventory is None, then only system admins and org admins can
|
||||||
|
# do anything with the JT in 2.4
|
||||||
|
continue
|
||||||
|
|
||||||
jt_permission_qs = Permission.objects.filter(
|
jt_permission_qs = Permission.objects.filter(
|
||||||
inventory=jt.inventory,
|
inventory=jt.inventory,
|
||||||
project=jt.project,
|
project=jt.project,
|
||||||
@@ -494,7 +475,6 @@ def migrate_job_templates(apps, schema_editor):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def rebuild_role_hierarchy(apps, schema_editor):
|
def rebuild_role_hierarchy(apps, schema_editor):
|
||||||
logger.info('Computing role roots..')
|
logger.info('Computing role roots..')
|
||||||
start = time()
|
start = time()
|
||||||
|
|||||||
@@ -9,25 +9,8 @@ from awx.fact.utils.dbtransform import KeyTransform
|
|||||||
from mongoengine.connection import ConnectionError
|
from mongoengine.connection import ConnectionError
|
||||||
from pymongo.errors import OperationFailure
|
from pymongo.errors import OperationFailure
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('system_tracking_migrations')
|
||||||
|
|
||||||
def log_migration(wrapped):
|
|
||||||
'''setup the logging mechanism for each migration method
|
|
||||||
as it runs, Django resets this, so we use a decorator
|
|
||||||
to re-add the handler for each method.
|
|
||||||
'''
|
|
||||||
handler = logging.FileHandler("/tmp/tower_system_tracking_migrations.log", mode="a", encoding="UTF-8")
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
handler.setLevel(logging.DEBUG)
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
logger.handlers = []
|
|
||||||
logger.addHandler(handler)
|
|
||||||
return wrapped(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_facts(apps, schema_editor):
|
def migrate_facts(apps, schema_editor):
|
||||||
Fact = apps.get_model('main', "Fact")
|
Fact = apps.get_model('main', "Fact")
|
||||||
Host = apps.get_model('main', "Host")
|
Host = apps.get_model('main', "Host")
|
||||||
@@ -52,7 +35,7 @@ def migrate_facts(apps, schema_editor):
|
|||||||
migrated_count = 0
|
migrated_count = 0
|
||||||
not_migrated_count = 0
|
not_migrated_count = 0
|
||||||
transform = KeyTransform([('.', '\uff0E'), ('$', '\uff04')])
|
transform = KeyTransform([('.', '\uff0E'), ('$', '\uff04')])
|
||||||
for factver in FactVersion.objects.all():
|
for factver in FactVersion.objects.all().no_cache():
|
||||||
try:
|
try:
|
||||||
host = Host.objects.only('id').get(inventory__id=factver.host.inventory_id, name=factver.host.hostname)
|
host = Host.objects.only('id').get(inventory__id=factver.host.inventory_id, name=factver.host.hostname)
|
||||||
fact_obj = transform.replace_outgoing(factver.fact)
|
fact_obj = transform.replace_outgoing(factver.fact)
|
||||||
|
|||||||
@@ -2,25 +2,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('rbac_migrations')
|
||||||
|
|
||||||
def log_migration(wrapped):
|
|
||||||
'''setup the logging mechanism for each migration method
|
|
||||||
as it runs, Django resets this, so we use a decorator
|
|
||||||
to re-add the handler for each method.
|
|
||||||
'''
|
|
||||||
handler = logging.FileHandler("/tmp/tower_rbac_migrations.log", mode="a", encoding="UTF-8")
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
handler.setLevel(logging.DEBUG)
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
logger.handlers = []
|
|
||||||
logger.addHandler(handler)
|
|
||||||
return wrapped(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
@log_migration
|
|
||||||
def migrate_team(apps, schema_editor):
|
def migrate_team(apps, schema_editor):
|
||||||
'''If an orphan team exists that is still active, delete it.'''
|
'''If an orphan team exists that is still active, delete it.'''
|
||||||
Team = apps.get_model('main', 'Team')
|
Team = apps.get_model('main', 'Team')
|
||||||
|
|||||||
@@ -873,7 +873,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
if not self.cancel_flag:
|
if not self.cancel_flag:
|
||||||
self.cancel_flag = True
|
self.cancel_flag = True
|
||||||
cancel_fields = ['cancel_flag']
|
cancel_fields = ['cancel_flag']
|
||||||
if self.status in ('pending', 'waiting'):
|
if self.status in ('pending', 'waiting', 'new'):
|
||||||
self.status = 'canceled'
|
self.status = 'canceled'
|
||||||
cancel_fields.append('status')
|
cancel_fields.append('status')
|
||||||
self.save(update_fields=cancel_fields)
|
self.save(update_fields=cancel_fields)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import json
|
|
||||||
|
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ class WebhookBackend(TowerBaseEmailBackend):
|
|||||||
self.headers['User-Agent'] = "Tower {}".format(get_awx_version())
|
self.headers['User-Agent'] = "Tower {}".format(get_awx_version())
|
||||||
for m in messages:
|
for m in messages:
|
||||||
r = requests.post("{}".format(m.recipients()[0]),
|
r = requests.post("{}".format(m.recipients()[0]),
|
||||||
data=json.dumps(m.body),
|
json=m.body,
|
||||||
headers=self.headers)
|
headers=self.headers)
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
logger.error(smart_text("Error sending notification webhook: {}".format(r.text)))
|
logger.error(smart_text("Error sending notification webhook: {}".format(r.text)))
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ from django.contrib.auth.models import User
|
|||||||
from awx.main.constants import CLOUD_PROVIDERS
|
from awx.main.constants import CLOUD_PROVIDERS
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
from awx.main.models import UnifiedJob
|
from awx.main.models import UnifiedJob
|
||||||
from awx.main.models.label import Label
|
|
||||||
from awx.main.queue import FifoQueue
|
from awx.main.queue import FifoQueue
|
||||||
from awx.main.conf import tower_settings
|
from awx.main.conf import tower_settings
|
||||||
from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL
|
from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL
|
||||||
@@ -123,13 +122,6 @@ def run_administrative_checks(self):
|
|||||||
tower_admin_emails,
|
tower_admin_emails,
|
||||||
fail_silently=True)
|
fail_silently=True)
|
||||||
|
|
||||||
@task(bind=True)
|
|
||||||
def run_label_cleanup(self):
|
|
||||||
qs = Label.get_orphaned_labels()
|
|
||||||
labels_count = qs.count()
|
|
||||||
qs.delete()
|
|
||||||
return labels_count
|
|
||||||
|
|
||||||
@task(bind=True)
|
@task(bind=True)
|
||||||
def cleanup_authtokens(self):
|
def cleanup_authtokens(self):
|
||||||
AuthToken.objects.filter(expires__lt=now()).delete()
|
AuthToken.objects.filter(expires__lt=now()).delete()
|
||||||
@@ -1307,9 +1299,11 @@ class RunInventoryUpdate(BaseTask):
|
|||||||
cp.set(section, 'password', decrypt_field(credential, 'password'))
|
cp.set(section, 'password', decrypt_field(credential, 'password'))
|
||||||
|
|
||||||
section = 'ansible'
|
section = 'ansible'
|
||||||
|
cp.add_section(section)
|
||||||
cp.set(section, 'group_patterns', '["{app}-{tier}-{color}", "{app}-{color}", "{app}", "{tier}"]')
|
cp.set(section, 'group_patterns', '["{app}-{tier}-{color}", "{app}-{color}", "{app}", "{tier}"]')
|
||||||
|
|
||||||
section = 'cache'
|
section = 'cache'
|
||||||
|
cp.add_section(section)
|
||||||
cp.set(section, 'path', '/tmp')
|
cp.set(section, 'path', '/tmp')
|
||||||
cp.set(section, 'max_age', '0')
|
cp.set(section, 'max_age', '0')
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def mock_feature_enabled(feature, bypass_database=None):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def role():
|
def role():
|
||||||
return Role.objects.create()
|
return Role.objects.create(role_field='admin_role')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -210,33 +210,33 @@ def test_get_teams_roles_list(get, team, organization, admin):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_add_role_to_teams(team, role, post, admin):
|
def test_add_role_to_teams(team, post, admin):
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 0
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 0
|
||||||
url = reverse('api:team_roles_list', args=(team.id,))
|
url = reverse('api:team_roles_list', args=(team.id,))
|
||||||
|
|
||||||
response = post(url, {'id': role.id}, admin)
|
response = post(url, {'id': team.member_role.id}, admin)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 1
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 1
|
||||||
|
|
||||||
response = post(url, {'id': role.id}, admin)
|
response = post(url, {'id': team.member_role.id}, admin)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 1
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 1
|
||||||
|
|
||||||
response = post(url, {}, admin)
|
response = post(url, {}, admin)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 1
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 1
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_remove_role_from_teams(team, role, post, admin):
|
def test_remove_role_from_teams(team, post, admin):
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 0
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 0
|
||||||
url = reverse('api:team_roles_list', args=(team.id,))
|
url = reverse('api:team_roles_list', args=(team.id,))
|
||||||
response = post(url, {'id': role.id}, admin)
|
response = post(url, {'id': team.member_role.id}, admin)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 1
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 1
|
||||||
|
|
||||||
response = post(url, {'disassociate': role.id, 'id': role.id}, admin)
|
response = post(url, {'disassociate': team.member_role.id, 'id': team.member_role.id}, admin)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert team.member_role.children.filter(id=role.id).count() == 0
|
assert team.member_role.children.filter(id=team.member_role.id).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ def test_team_attach_unattach(team, user):
|
|||||||
access = TeamAccess(u)
|
access = TeamAccess(u)
|
||||||
|
|
||||||
team.member_role.members.add(u)
|
team.member_role.members.add(u)
|
||||||
assert not access.can_attach(team, u.admin_role, 'member_role.children', None)
|
assert not access.can_attach(team, team.member_role, 'member_role.children', None)
|
||||||
assert not access.can_unattach(team, u.admin_role, 'member_role.children')
|
assert not access.can_unattach(team, team.member_role, 'member_role.children')
|
||||||
|
|
||||||
team.admin_role.members.add(u)
|
team.admin_role.members.add(u)
|
||||||
assert access.can_attach(team, u.admin_role, 'member_role.children', None)
|
assert access.can_attach(team, team.member_role, 'member_role.children', None)
|
||||||
assert access.can_unattach(team, u.admin_role, 'member_role.children')
|
assert access.can_unattach(team, team.member_role, 'member_role.children')
|
||||||
|
|
||||||
u2 = user('non-member', False)
|
u2 = user('non-member', False)
|
||||||
access = TeamAccess(u2)
|
access = TeamAccess(u2)
|
||||||
assert not access.can_attach(team, u2.admin_role, 'member_role.children', None)
|
assert not access.can_attach(team, team.member_role, 'member_role.children', None)
|
||||||
assert not access.can_unattach(team, u2.admin_role, 'member_role.chidlren')
|
assert not access.can_unattach(team, team.member_role, 'member_role.chidlren')
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_team_access_superuser(team, user):
|
def test_team_access_superuser(team, user):
|
||||||
|
|||||||
@@ -75,3 +75,16 @@ def test_org_user_removed(user, organization):
|
|||||||
|
|
||||||
organization.member_role.members.remove(member)
|
organization.member_role.members.remove(member)
|
||||||
assert admin not in member.admin_role
|
assert admin not in member.admin_role
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_org_admin_create_sys_auditor(org_admin):
|
||||||
|
access = UserAccess(org_admin)
|
||||||
|
assert not access.can_add(data=dict(
|
||||||
|
username='new_user', password="pa$$sowrd", email="asdf@redhat.com",
|
||||||
|
is_system_auditor='true'))
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_org_admin_edit_sys_auditor(org_admin, alice, organization):
|
||||||
|
organization.member_role.members.add(alice)
|
||||||
|
access = UserAccess(org_admin)
|
||||||
|
assert not access.can_change(obj=alice, data=dict(is_system_auditor='true'))
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from django.conf import settings
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@pytest.mark.parametrize("job_name,function_path", [
|
@pytest.mark.parametrize("job_name,function_path", [
|
||||||
('label_cleanup', 'awx.main.tasks.run_label_cleanup'),
|
|
||||||
('admin_checks', 'awx.main.tasks.run_administrative_checks'),
|
('admin_checks', 'awx.main.tasks.run_administrative_checks'),
|
||||||
('tower_scheduler', 'awx.main.tasks.tower_periodic_scheduler'),
|
('tower_scheduler', 'awx.main.tasks.tower_periodic_scheduler'),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from awx.main.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from awx.main.tasks import (
|
from awx.main.tasks import (
|
||||||
run_label_cleanup,
|
|
||||||
send_notifications,
|
send_notifications,
|
||||||
run_administrative_checks,
|
run_administrative_checks,
|
||||||
)
|
)
|
||||||
@@ -21,16 +20,6 @@ def apply_patches(_patches):
|
|||||||
yield
|
yield
|
||||||
[p.stop() for p in _patches]
|
[p.stop() for p in _patches]
|
||||||
|
|
||||||
def test_run_label_cleanup(mocker):
|
|
||||||
qs = mocker.Mock(**{'count.return_value': 3, 'delete.return_value': None})
|
|
||||||
mock_label = mocker.patch('awx.main.models.label.Label.get_orphaned_labels',return_value=qs)
|
|
||||||
|
|
||||||
ret = run_label_cleanup()
|
|
||||||
|
|
||||||
mock_label.assert_called_with()
|
|
||||||
qs.delete.assert_called_with()
|
|
||||||
assert 3 == ret
|
|
||||||
|
|
||||||
def test_send_notifications_not_list():
|
def test_send_notifications_not_list():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
send_notifications(None)
|
send_notifications(None)
|
||||||
|
|||||||
0
awx/plugins/inventory/cloudforms.py
Normal file → Executable file
0
awx/plugins/inventory/cloudforms.py
Normal file → Executable file
3
awx/plugins/inventory/foreman.py
Normal file → Executable file
3
awx/plugins/inventory/foreman.py
Normal file → Executable file
@@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
# vim: set fileencoding=utf-8 :
|
||||||
#
|
#
|
||||||
# NOTE FOR TOWER: change foreman_ to sattelite_ for the group prefix
|
# NOTE FOR TOWER: change foreman_ to sattelite_ for the group prefix
|
||||||
#
|
#
|
||||||
# vim: set fileencoding=utf-8 :
|
|
||||||
#
|
|
||||||
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
|
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
|
||||||
#
|
#
|
||||||
# This script is free software: you can redistribute it and/or modify
|
# This script is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -345,10 +345,6 @@ CELERYBEAT_SCHEDULE = {
|
|||||||
'task': 'awx.main.tasks.run_administrative_checks',
|
'task': 'awx.main.tasks.run_administrative_checks',
|
||||||
'schedule': timedelta(days=30)
|
'schedule': timedelta(days=30)
|
||||||
},
|
},
|
||||||
'label_cleanup': {
|
|
||||||
'task': 'awx.main.tasks.run_label_cleanup',
|
|
||||||
'schedule': timedelta(days=7)
|
|
||||||
},
|
|
||||||
'authtoken_cleanup': {
|
'authtoken_cleanup': {
|
||||||
'task': 'awx.main.tasks.cleanup_authtokens',
|
'task': 'awx.main.tasks.cleanup_authtokens',
|
||||||
'schedule': timedelta(days=30)
|
'schedule': timedelta(days=30)
|
||||||
@@ -676,6 +672,16 @@ OPENSTACK_HOST_FILTER = r'^.+$'
|
|||||||
OPENSTACK_EXCLUDE_EMPTY_GROUPS = True
|
OPENSTACK_EXCLUDE_EMPTY_GROUPS = True
|
||||||
OPENSTACK_INSTANCE_ID_VAR = 'openstack.id'
|
OPENSTACK_INSTANCE_ID_VAR = 'openstack.id'
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# ----- Foreman -----
|
||||||
|
# ---------------------
|
||||||
|
SATELLITE6_ENABLED_VAR = 'foreman.enabled'
|
||||||
|
SATELLITE6_ENABLED_VALUE = 'true'
|
||||||
|
SATELLITE6_GROUP_FILTER = r'^.+$'
|
||||||
|
SATELLITE6_HOST_FILTER = r'^.+$'
|
||||||
|
SATELLITE6_EXCLUDE_EMPTY_GROUPS = True
|
||||||
|
SATELLITE6_INSTANCE_ID_VAR = 'foreman.id'
|
||||||
|
|
||||||
# ---------------------
|
# ---------------------
|
||||||
# -- Activity Stream --
|
# -- Activity Stream --
|
||||||
# ---------------------
|
# ---------------------
|
||||||
@@ -941,7 +947,34 @@ LOGGING = {
|
|||||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
||||||
'backupCount': 5,
|
'backupCount': 5,
|
||||||
'formatter':'simple',
|
'formatter':'simple',
|
||||||
}
|
},
|
||||||
|
'fact_receiver': {
|
||||||
|
'level': 'WARNING',
|
||||||
|
'class':'logging.handlers.RotatingFileHandler',
|
||||||
|
'filters': ['require_debug_false'],
|
||||||
|
'filename': os.path.join(LOG_ROOT, 'fact_receiver.log'),
|
||||||
|
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
||||||
|
'backupCount': 5,
|
||||||
|
'formatter':'simple',
|
||||||
|
},
|
||||||
|
'system_tracking_migrations': {
|
||||||
|
'level': 'WARNING',
|
||||||
|
'class':'logging.handlers.RotatingFileHandler',
|
||||||
|
'filters': ['require_debug_false'],
|
||||||
|
'filename': os.path.join(LOG_ROOT, 'tower_system_tracking_migrations.log'),
|
||||||
|
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
||||||
|
'backupCount': 5,
|
||||||
|
'formatter':'simple',
|
||||||
|
},
|
||||||
|
'rbac_migrations': {
|
||||||
|
'level': 'WARNING',
|
||||||
|
'class':'logging.handlers.RotatingFileHandler',
|
||||||
|
'filters': ['require_debug_false'],
|
||||||
|
'filename': os.path.join(LOG_ROOT, 'tower_rbac_migrations.log'),
|
||||||
|
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
||||||
|
'backupCount': 5,
|
||||||
|
'formatter':'simple',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
'django': {
|
'django': {
|
||||||
@@ -1000,6 +1033,14 @@ LOGGING = {
|
|||||||
'handlers': ['console', 'file', 'tower_warnings'],
|
'handlers': ['console', 'file', 'tower_warnings'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
},
|
},
|
||||||
|
'system_tracking_migrations': {
|
||||||
|
'handlers': ['console', 'file', 'tower_warnings'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
|
'rbac_migrations': {
|
||||||
|
'handlers': ['console', 'file', 'tower_warnings'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,56 +49,13 @@ ANSIBLE_VENV_PATH = "/var/lib/awx/venv/ansible"
|
|||||||
TOWER_USE_VENV = True
|
TOWER_USE_VENV = True
|
||||||
TOWER_VENV_PATH = "/var/lib/awx/venv/tower"
|
TOWER_VENV_PATH = "/var/lib/awx/venv/tower"
|
||||||
|
|
||||||
LOGGING['handlers']['tower_warnings'] = {
|
LOGGING['handlers']['tower_warnings']['filename'] = '/var/log/tower/tower.log'
|
||||||
'level': 'WARNING',
|
LOGGING['handlers']['callback_receiver']['filename'] = '/var/log/tower/callback_receiver.log'
|
||||||
'class':'logging.handlers.RotatingFileHandler',
|
LOGGING['handlers']['socketio_service']['filename'] = '/var/log/tower/socketio_service.log'
|
||||||
'filters': ['require_debug_false'],
|
LOGGING['handlers']['task_system']['filename'] = '/var/log/tower/task_system.log'
|
||||||
'filename': '/var/log/tower/tower.log',
|
LOGGING['handlers']['fact_receiver']['filename'] = '/var/log/tower/fact_receiver.log'
|
||||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
LOGGING['handlers']['system_tracking_migrations']['filename'] = '/var/log/tower/tower_system_tracking_migrations.log'
|
||||||
'backupCount': 5,
|
LOGGING['handlers']['rbac_migrations']['filename'] = '/var/log/tower/tower_rbac_migrations.log'
|
||||||
'formatter':'simple',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
LOGGING['handlers']['callback_receiver'] = {
|
|
||||||
'level': 'WARNING',
|
|
||||||
'class':'logging.handlers.RotatingFileHandler',
|
|
||||||
'filters': ['require_debug_false'],
|
|
||||||
'filename': '/var/log/tower/callback_receiver.log',
|
|
||||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
|
||||||
'backupCount': 5,
|
|
||||||
'formatter':'simple',
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGING['handlers']['socketio_service'] = {
|
|
||||||
'level': 'WARNING',
|
|
||||||
'class':'logging.handlers.RotatingFileHandler',
|
|
||||||
'filters': ['require_debug_false'],
|
|
||||||
'filename': '/var/log/tower/socketio_service.log',
|
|
||||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
|
||||||
'backupCount': 5,
|
|
||||||
'formatter':'simple',
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGING['handlers']['task_system'] = {
|
|
||||||
'level': 'INFO',
|
|
||||||
'class':'logging.handlers.RotatingFileHandler',
|
|
||||||
'filters': ['require_debug_false'],
|
|
||||||
'filename': '/var/log/tower/task_system.log',
|
|
||||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
|
||||||
'backupCount': 5,
|
|
||||||
'formatter':'simple',
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGING['handlers']['fact_receiver'] = {
|
|
||||||
'level': 'WARNING',
|
|
||||||
'class':'logging.handlers.RotatingFileHandler',
|
|
||||||
'filters': ['require_debug_false'],
|
|
||||||
'filename': '/var/log/tower/fact_receiver.log',
|
|
||||||
'maxBytes': 1024 * 1024 * 5, # 5 MB
|
|
||||||
'backupCount': 5,
|
|
||||||
'formatter':'simple',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load settings from any .py files in the global conf.d directory specified in
|
# Load settings from any .py files in the global conf.d directory specified in
|
||||||
# the environment, defaulting to /etc/tower/conf.d/.
|
# the environment, defaulting to /etc/tower/conf.d/.
|
||||||
|
|||||||
@@ -872,7 +872,7 @@ var tower = angular.module('Tower', [
|
|||||||
} else {
|
} else {
|
||||||
var lastUser = $cookieStore.get('current_user'),
|
var lastUser = $cookieStore.get('current_user'),
|
||||||
timestammp = Store('sessionTime');
|
timestammp = Store('sessionTime');
|
||||||
if(lastUser && lastUser.id && timestammp && timestammp[lastUser.id]){
|
if(lastUser && lastUser.id && timestammp && timestammp[lastUser.id] && timestammp[lastUser.id].loggedIn){
|
||||||
var stime = timestammp[lastUser.id].time,
|
var stime = timestammp[lastUser.id].time,
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
if ((stime - now) <= 0) {
|
if ((stime - now) <= 0) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function JobsListController ($rootScope, $log, $scope, $compile, $statePa
|
|||||||
list: AllJobsList,
|
list: AllJobsList,
|
||||||
id: 'active-jobs',
|
id: 'active-jobs',
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
url: GetBasePath('unified_jobs') + '?status__in=pending,waiting,running,completed,failed,successful,error,canceled&order_by=-finished',
|
url: GetBasePath('unified_jobs') + '?status__in=pending,waiting,running,completed,failed,successful,error,canceled,new&order_by=-finished',
|
||||||
searchParams: search_params,
|
searchParams: search_params,
|
||||||
spinner: false
|
spinner: false
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
|
import sys
|
||||||
|
if sys.prefix != '/var/lib/awx/venv/tower':
|
||||||
|
raise RuntimeError('Tower virtualenv not activated. Check WSGIPythonHome in Apache configuration.')
|
||||||
from awx.wsgi import application # NOQA
|
from awx.wsgi import application # NOQA
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
# All Rights Reserved
|
# All Rights Reserved
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings.development") # noqa
|
|
||||||
|
|
||||||
import django
|
|
||||||
django.setup() # noqa
|
|
||||||
|
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -15,7 +10,7 @@ from optparse import make_option, OptionParser
|
|||||||
|
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
|
import django
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -23,7 +18,8 @@ from django.db import transaction
|
|||||||
# awx
|
# awx
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings.development") # noqa
|
||||||
|
django.setup() # noqa
|
||||||
|
|
||||||
|
|
||||||
option_list = [
|
option_list = [
|
||||||
|
|||||||
Reference in New Issue
Block a user