Merge branch 'release_3.0.3' into devel

* release_3.0.3: (55 commits)
  Revert "Revert "Add needed types for selinux change""
  Revert "Add needed types for selinux change"
  interpret backslash escapes when displaying url in welcome message
  Bump the SELinux policy version
  Add needed types for selinux change
  Update SELinux policy to allow httpd_t to execute files in lib_t and var_lib_t
  Bumping changelog for 3.0.3
  Update rax.py inventory
  Revert "filter internal User.admin_roles from the /roles API list view"
  fix spelling of disassociated
  Resolves 404 when assigning resources/users to organizations in card view. Sidesteps a bug in the Refresh() utility, where pagination calculations are not made against filtered results.
  Sync azure changes to Tower virtual environment
  Add regions here as well.
  Also bump boto for new regions, per ryansb.
  More regions!
  Revert "bump shade version"
  bump shade version
  Hack copying of job_template.related.survey_spec into ui job copy flow, resolves #3737
  Revert "bump shade version"
  bump shade version
  ...
This commit is contained in:
Matthew Jones 2016-11-01 11:49:28 -04:00
commit c6cf02a602
29 changed files with 442 additions and 223 deletions

View File

@ -14,7 +14,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text
# Django REST Framework
from rest_framework.exceptions import ParseError
from rest_framework.exceptions import ParseError, PermissionDenied
from rest_framework.filters import BaseFilterBackend
# Ansible Tower
@ -97,7 +97,10 @@ class FieldLookupBackend(BaseFilterBackend):
new_parts.append(name)
if name == 'pk':
if name in getattr(model, 'PASSWORD_FIELDS', ()):
raise PermissionDenied('Filtering on password fields is not allowed.')
elif name == 'pk':
field = model._meta.pk
else:
field = model._meta.get_field_by_name(name)[0]

View File

@ -2,7 +2,7 @@
Labels not associated with any other resources are deleted. A label can become disassociated with a resource as a result of 3 events.
1. A label is explicitly diassociated with a related job template
1. A label is explicitly disassociated with a related job template
2. A job is deleted with labels
3. A cleanup job deletes a job with labels

View File

@ -764,7 +764,6 @@ class CredentialAccess(BaseAccess):
or (not organization_pk and obj.organization):
return False
print(self.user in obj.admin_role)
return self.user in obj.admin_role
def can_delete(self, obj):
@ -1237,17 +1236,32 @@ class JobAccess(BaseAccess):
if self.user.is_superuser:
return True
# If a user can launch the job template then they can relaunch a job from that
# job template
inventory_access = obj.inventory and self.user in obj.inventory.use_role
credential_access = obj.credential and self.user in obj.credential.use_role
# Check if JT execute access (and related prompts) is sufficient
if obj.job_template is not None:
return self.user in obj.job_template.execute_role
prompts_access = True
job_fields = {}
for fd in obj.job_template._ask_for_vars_dict():
job_fields[fd] = getattr(obj, fd)
accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields)
for fd in ignored_fields:
if fd != 'extra_vars' and job_fields[fd] != getattr(obj.job_template, fd):
# Job has field that is not promptable
prompts_access = False
if obj.credential != obj.job_template.credential and not credential_access:
prompts_access = False
if obj.inventory != obj.job_template.inventory and not inventory_access:
prompts_access = False
if prompts_access and self.user in obj.job_template.execute_role:
return True
inventory_access = self.user in obj.inventory.use_role
credential_access = self.user in obj.credential.use_role
org_access = self.user in obj.inventory.organization.admin_role
org_access = obj.inventory and self.user in obj.inventory.organization.admin_role
project_access = obj.project is None or self.user in obj.project.admin_role
# job can be relaunched if user could make an equivalent JT
return inventory_access and credential_access and (org_access or project_access)
def can_cancel(self, obj):

View File

@ -51,7 +51,6 @@ class CallbackBrokerWorker(ConsumerMixin):
logger.error('Callback Task Processor Raised Exception: %r', exc)
message.ack()
class Command(NoArgsCommand):
'''
Save Job Callback receiver (see awx.plugins.callbacks.job_event_callback)

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from awx.main.migrations import _migration_utils as migration_utils
def update_dashed_host_variables(apps, schema_editor):
Host = apps.get_model('main', 'Host')
for host in Host.objects.filter(variables='---'):
host.variables = ''
host.save()
class Migration(migrations.Migration):
dependencies = [
('main', '0032_v302_credential_permissions_update'),
]
operations = [
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
migrations.RunPython(update_dashed_host_variables),
]

View File

@ -551,7 +551,7 @@ class BaseTask(Task):
output_replacements=output_replacements)
job_start = time.time()
while child.isalive():
result_id = child.expect(expect_list, timeout=pexpect_timeout)
result_id = child.expect(expect_list, timeout=pexpect_timeout, searchwindowsize=100)
if result_id in expect_passwords:
child.sendline(expect_passwords[result_id])
if logfile_pos != logfile.tell():

View File

@ -2,11 +2,7 @@ import pytest
from awx.main.models.inventory import Inventory
from awx.main.models.credential import Credential
from awx.main.models.jobs import JobTemplate
@pytest.fixture
def machine_credential():
return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word')
from awx.main.models.jobs import JobTemplate, Job
@pytest.mark.django_db
@pytest.mark.job_permissions
@ -45,3 +41,52 @@ def test_inventory_use_access(inventory, user):
inventory.use_role.members.add(common_user)
assert common_user.can_access(Inventory, 'use', inventory)
@pytest.mark.django_db
class TestJobRelaunchAccess:
@pytest.fixture
def job_no_prompts(self, machine_credential, inventory):
jt = JobTemplate.objects.create(name='test-job_template', credential=machine_credential, inventory=inventory)
return jt.create_unified_job()
@pytest.fixture
def job_with_prompts(self, machine_credential, inventory, organization):
jt = JobTemplate.objects.create(
name='test-job-template-prompts', credential=machine_credential, inventory=inventory,
ask_tags_on_launch=True, ask_variables_on_launch=True, ask_skip_tags_on_launch=True,
ask_limit_on_launch=True, ask_job_type_on_launch=True, ask_inventory_on_launch=True,
ask_credential_on_launch=True)
new_cred = Credential.objects.create(name='new-cred', kind='ssh', username='test_user', password='pas4word')
new_inv = Inventory.objects.create(name='new-inv', organization=organization)
return jt.create_unified_job(credential=new_cred, inventory=new_inv)
def test_normal_relaunch_via_job_template(self, job_no_prompts, rando):
"Has JT execute_role, job unchanged relative to JT"
job_no_prompts.job_template.execute_role.members.add(rando)
assert rando.can_access(Job, 'start', job_no_prompts)
def test_no_relaunch_without_prompted_fields_access(self, job_with_prompts, rando):
"Has JT execute_role but no use_role on inventory & credential - deny relaunch"
job_with_prompts.job_template.execute_role.members.add(rando)
assert not rando.can_access(Job, 'start', job_with_prompts)
def test_can_relaunch_with_prompted_fields_access(self, job_with_prompts, rando):
"Has use_role on the prompted inventory & credential - allow relaunch"
job_with_prompts.job_template.execute_role.members.add(rando)
job_with_prompts.credential.use_role.members.add(rando)
job_with_prompts.inventory.use_role.members.add(rando)
assert rando.can_access(Job, 'start', job_with_prompts)
def test_no_relaunch_after_limit_change(self, job_no_prompts, rando):
"State of the job contradicts the JT state - deny relaunch"
job_no_prompts.job_template.execute_role.members.add(rando)
job_no_prompts.limit = 'webservers'
job_no_prompts.save()
assert not rando.can_access(Job, 'start', job_no_prompts)
def test_can_relaunch_if_limit_was_prompt(self, job_with_prompts, rando):
"Job state differs from JT, but only on prompted fields - allow relaunch"
job_with_prompts.job_template.execute_role.members.add(rando)
job_with_prompts.limit = 'webservers'
job_with_prompts.save()
assert not rando.can_access(Job, 'start', job_with_prompts)

View File

@ -1,7 +1,8 @@
import pytest
from rest_framework.exceptions import PermissionDenied
from awx.api.filters import FieldLookupBackend
from awx.main.models import JobTemplate
from awx.main.models import Credential, JobTemplate
@pytest.mark.parametrize(u"empty_value", [u'', ''])
def test_empty_in(empty_value):
@ -15,3 +16,21 @@ def test_valid_in(valid_value):
field_lookup = FieldLookupBackend()
value, new_lookup = field_lookup.value_to_python(JobTemplate, 'project__in', valid_value)
assert 'foo' in value
@pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in'])
@pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS)
def test_filter_on_password_field(password_field, lookup_suffix):
field_lookup = FieldLookupBackend()
lookup = '__'.join(filter(None, [password_field, lookup_suffix]))
with pytest.raises(PermissionDenied) as excinfo:
field, new_lookup = field_lookup.get_field_from_lookup(Credential, lookup)
assert 'not allowed' in str(excinfo.value)
@pytest.mark.parametrize('lookup_suffix', ['', 'contains', 'startswith', 'in'])
@pytest.mark.parametrize('password_field', Credential.PASSWORD_FIELDS)
def test_filter_on_related_password_field(password_field, lookup_suffix):
field_lookup = FieldLookupBackend()
lookup = '__'.join(filter(None, ['credential', password_field, lookup_suffix]))
with pytest.raises(PermissionDenied) as excinfo:
field, new_lookup = field_lookup.get_field_from_lookup(JobTemplate, lookup)
assert 'not allowed' in str(excinfo.value)

View File

@ -9,6 +9,15 @@ from awx.main.access import (
check_superuser,
JobTemplateAccess,
WorkflowJobTemplateAccess,
SystemJobTemplateAccess,
)
from awx.main.models import (
Credential,
Inventory,
Project,
Role,
Organization,
)
from awx.conf.license import LicenseForbids
from awx.main.models import Credential, Inventory, Project, Role, Organization, Instance
@ -124,7 +133,6 @@ def test_jt_can_add_bad_data(user_unit):
access = JobTemplateAccess(user_unit)
assert not access.can_add({'asdf': 'asdf'})
class TestWorkflowAccessMethods:
@pytest.fixture
def workflow(self, workflow_job_template_factory):
@ -172,3 +180,12 @@ def test_user_capabilities_method():
'copy': 'foobar'
}
def test_system_job_template_can_start(mocker):
user = mocker.MagicMock(spec=User, id=1, is_system_auditor=True, is_superuser=False)
assert user.is_system_auditor
access = SystemJobTemplateAccess(user)
assert not access.can_start(None)
user.is_superuser = True
access = SystemJobTemplateAccess(user)
assert access.can_start(None)

View File

@ -0,0 +1,11 @@
from split_settings.tools import include
def test_postprocess_auth_basic_enabled():
locals().update({'__file__': __file__})
include('../../../settings/defaults.py', scope=locals())
assert 'awx.api.authentication.LoggedBasicAuthentication' in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES']
locals().update({'AUTH_BASIC_ENABLED': False})
include('../../../settings/postprocess.py', scope=locals())
assert 'awx.api.authentication.LoggedBasicAuthentication' not in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES']

View File

@ -1,5 +1,5 @@
#
# Configuration file for azure_rm_invetory.py
# Configuration file for azure_rm.py
#
[azure]
# Control which resource groups are included. By default all resources groups are included.
@ -9,11 +9,14 @@
# Control which tags are included. Set tags to a comma separated list of keys or key:value pairs
#tags=
# Control which locations are included. Set locations to a comma separated list (e.g. eastus,eastus2,westus)
#locations=
# Include powerstate. If you don't need powerstate information, turning it off improves runtime performance.
include_powerstate=yes
# Control grouping with the following boolean flags. Valid values: yes, no, true, false, True, False, 0, 1.
group_by_resource_group=yes
group_by_location=yes
group_by_security_group=no
group_by_security_group=yes
group_by_tag=yes

View File

@ -76,7 +76,7 @@ required. For a specific host, this script returns the following variables:
"version": "latest"
},
"location": "westus",
"mac_address": "00-0D-3A-31-2C-EC",
"mac_address": "00-00-5E-00-53-FE",
"name": "object-name",
"network_interface": "interface-name",
"network_interface_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkInterfaces/object-name1",
@ -115,7 +115,7 @@ When run in --list mode, instances are grouped by the following categories:
- tag key
- tag key_value
Control groups using azure_rm_inventory.ini or set environment variables:
Control groups using azure_rm.ini or set environment variables:
AZURE_GROUP_BY_RESOURCE_GROUP=yes
AZURE_GROUP_BY_LOCATION=yes
@ -130,6 +130,10 @@ Select hosts for specific tag key by assigning a comma separated list of tag key
AZURE_TAGS=key1,key2,key3
Select hosts for specific locations:
AZURE_LOCATIONS=eastus,westus,eastus2
Or, select hosts for specific tag key:value pairs by assigning a comma separated list key:value pairs to:
AZURE_TAGS=key1:value1,key2:value2
@ -137,12 +141,14 @@ AZURE_TAGS=key1:value1,key2:value2
If you don't need the powerstate, you can improve performance by turning off powerstate fetching:
AZURE_INCLUDE_POWERSTATE=no
azure_rm_inventory.ini
----------------------
As mentioned above you can control execution using environment variables or an .ini file. A sample
azure_rm_inventory.ini is included. The name of the .ini file is the basename of the inventory script (in this case
'azure_rm_inventory') with a .ini extension. This provides you with the flexibility of copying and customizing this
script and having matching .ini files. Go forth and customize your Azure inventory!
azure_rm.ini
------------
As mentioned above, you can control execution using environment variables or a .ini file. A sample
azure_rm.ini is included. The name of the .ini file is the basename of the inventory script (in this case
'azure_rm') with a .ini extension. It also assumes the .ini file is alongside the script. To specify
a different path for the .ini file, define the AZURE_INI_PATH environment variable:
export AZURE_INI_PATH=/path/to/custom.ini
Powerstate:
-----------
@ -152,13 +158,13 @@ up. If the value is anything other than 'running', the machine is down, and will
Examples:
---------
Execute /bin/uname on all instances in the galaxy-qa resource group
$ ansible -i azure_rm_inventory.py galaxy-qa -m shell -a "/bin/uname -a"
$ ansible -i azure_rm.py galaxy-qa -m shell -a "/bin/uname -a"
Use the inventory script to print instance specific information
$ contrib/inventory/azure_rm_inventory.py --host my_instance_host_name --pretty
$ contrib/inventory/azure_rm.py --host my_instance_host_name --pretty
Use with a playbook
$ ansible-playbook -i contrib/inventory/azure_rm_inventory.py my_playbook.yml --limit galaxy-qa
$ ansible-playbook -i contrib/inventory/azure_rm.py my_playbook.yml --limit galaxy-qa
Insecure Platform Warning
@ -180,11 +186,13 @@ Version: 1.0.0
import argparse
import ConfigParser
import json
import json
import os
import re
import sys
from distutils.version import LooseVersion
from os.path import expanduser
HAS_AZURE = True
@ -195,12 +203,9 @@ try:
from azure.mgmt.compute import __version__ as azure_compute_version
from azure.common import AzureMissingResourceHttpError, AzureHttpError
from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials
from azure.mgmt.network.network_management_client import NetworkManagementClient,\
NetworkManagementClientConfiguration
from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient,\
ResourceManagementClientConfiguration
from azure.mgmt.compute.compute_management_client import ComputeManagementClient,\
ComputeManagementClientConfiguration
from azure.mgmt.network.network_management_client import NetworkManagementClient
from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient
from azure.mgmt.compute.compute_management_client import ComputeManagementClient
except ImportError as exc:
HAS_AZURE_EXC = exc
HAS_AZURE = False
@ -219,6 +224,7 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict(
AZURE_CONFIG_SETTINGS = dict(
resource_groups='AZURE_RESOURCE_GROUPS',
tags='AZURE_TAGS',
locations='AZURE_LOCATIONS',
include_powerstate='AZURE_INCLUDE_POWERSTATE',
group_by_resource_group='AZURE_GROUP_BY_RESOURCE_GROUP',
group_by_location='AZURE_GROUP_BY_LOCATION',
@ -226,7 +232,7 @@ AZURE_CONFIG_SETTINGS = dict(
group_by_tag='AZURE_GROUP_BY_TAG'
)
AZURE_MIN_VERSION = "2016-03-30"
AZURE_MIN_VERSION = "0.30.0rc5"
def azure_id_to_dict(id):
@ -362,8 +368,7 @@ class AzureRM(object):
def network_client(self):
self.log('Getting network client')
if not self._network_client:
self._network_client = NetworkManagementClient(
NetworkManagementClientConfiguration(self.azure_credentials, self.subscription_id))
self._network_client = NetworkManagementClient(self.azure_credentials, self.subscription_id)
self._register('Microsoft.Network')
return self._network_client
@ -371,16 +376,14 @@ class AzureRM(object):
def rm_client(self):
self.log('Getting resource manager client')
if not self._resource_client:
self._resource_client = ResourceManagementClient(
ResourceManagementClientConfiguration(self.azure_credentials, self.subscription_id))
self._resource_client = ResourceManagementClient(self.azure_credentials, self.subscription_id)
return self._resource_client
@property
def compute_client(self):
self.log('Getting compute client')
if not self._compute_client:
self._compute_client = ComputeManagementClient(
ComputeManagementClientConfiguration(self.azure_credentials, self.subscription_id))
self._compute_client = ComputeManagementClient(self.azure_credentials, self.subscription_id)
self._register('Microsoft.Compute')
return self._compute_client
@ -403,6 +406,7 @@ class AzureInventory(object):
self.resource_groups = []
self.tags = None
self.locations = None
self.replace_dash_in_groups = False
self.group_by_resource_group = True
self.group_by_location = True
@ -425,6 +429,9 @@ class AzureInventory(object):
if self._args.tags:
self.tags = self._args.tags.split(',')
if self._args.locations:
self.locations = self._args.locations.split(',')
if self._args.no_powerstate:
self.include_powerstate = False
@ -462,6 +469,8 @@ class AzureInventory(object):
help='Return inventory for comma separated list of resource group names')
parser.add_argument('--tags', action='store',
help='Return inventory for comma separated list of tag key:value pairs')
parser.add_argument('--locations', action='store',
help='Return inventory for comma separated list of locations')
parser.add_argument('--no-powerstate', action='store_true', default=False,
help='Do not include the power state of each virtual host')
return parser.parse_args()
@ -487,7 +496,7 @@ class AzureInventory(object):
except Exception as exc:
sys.exit("Error: fetching virtual machines - {0}".format(str(exc)))
if self._args.host or self.tags > 0:
if self._args.host or self.tags or self.locations:
selected_machines = self._selected_machines(virtual_machines)
self._load_machines(selected_machines)
else:
@ -524,7 +533,7 @@ class AzureInventory(object):
resource_group=resource_group,
mac_address=None,
plan=(machine.plan.name if machine.plan else None),
virtual_machine_size=machine.hardware_profile.vm_size.value,
virtual_machine_size=machine.hardware_profile.vm_size,
computer_name=machine.os_profile.computer_name,
provisioning_state=machine.provisioning_state,
)
@ -576,7 +585,7 @@ class AzureInventory(object):
host_vars['mac_address'] = network_interface.mac_address
for ip_config in network_interface.ip_configurations:
host_vars['private_ip'] = ip_config.private_ip_address
host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method.value
host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method
if ip_config.public_ip_address:
public_ip_reference = self._parse_ref_id(ip_config.public_ip_address.id)
public_ip_address = self._network_client.public_ip_addresses.get(
@ -585,7 +594,7 @@ class AzureInventory(object):
host_vars['ansible_host'] = public_ip_address.ip_address
host_vars['public_ip'] = public_ip_address.ip_address
host_vars['public_ip_name'] = public_ip_address.name
host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method.value
host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method
host_vars['public_ip_id'] = public_ip_address.id
if public_ip_address.dns_settings:
host_vars['fqdn'] = public_ip_address.dns_settings.fqdn
@ -599,6 +608,8 @@ class AzureInventory(object):
selected_machines.append(machine)
if self.tags and self._tags_match(machine.tags, self.tags):
selected_machines.append(machine)
if self.locations and machine.location in self.locations:
selected_machines.append(machine)
return selected_machines
def _get_security_groups(self, resource_group):
@ -676,17 +687,17 @@ class AzureInventory(object):
file_settings = self._load_settings()
if file_settings:
for key in AZURE_CONFIG_SETTINGS:
if key in ('resource_groups', 'tags') and file_settings.get(key, None) is not None:
if key in ('resource_groups', 'tags', 'locations') and file_settings.get(key):
values = file_settings.get(key).split(',')
if len(values) > 0:
setattr(self, key, values)
elif file_settings.get(key, None) is not None:
elif file_settings.get(key):
val = self._to_boolean(file_settings[key])
setattr(self, key, val)
else:
env_settings = self._get_env_settings()
for key in AZURE_CONFIG_SETTINGS:
if key in('resource_groups', 'tags') and env_settings.get(key, None) is not None:
if key in('resource_groups', 'tags', 'locations') and env_settings.get(key):
values = env_settings.get(key).split(',')
if len(values) > 0:
setattr(self, key, values)
@ -719,7 +730,8 @@ class AzureInventory(object):
def _load_settings(self):
basename = os.path.splitext(os.path.basename(__file__))[0]
path = basename + '.ini'
default_path = os.path.join(os.path.dirname(__file__), (basename + '.ini'))
path = os.path.expanduser(os.path.expandvars(os.environ.get('AZURE_INI_PATH', default_path)))
config = None
settings = None
try:
@ -774,11 +786,11 @@ class AzureInventory(object):
def main():
if not HAS_AZURE:
sys.exit("The Azure python sdk is not installed (try 'pip install azure') - {0}".format(HAS_AZURE_EXC))
sys.exit("The Azure python sdk is not installed (try 'pip install azure==2.0.0rc5') - {0}".format(HAS_AZURE_EXC))
if azure_compute_version < AZURE_MIN_VERSION:
sys.exit("Expecting azure.mgmt.compute.__version__ to be >= {0}. Found version {1} "
"Do you have Azure >= 2.0.0rc2 installed?".format(AZURE_MIN_VERSION, azure_compute_version))
if LooseVersion(azure_compute_version) != LooseVersion(AZURE_MIN_VERSION):
sys.exit("Expecting azure.mgmt.compute.__version__ to be {0}. Found version {1} "
"Do you have Azure == 2.0.0rc5 installed?".format(AZURE_MIN_VERSION, azure_compute_version))
AzureInventory()

View File

@ -155,8 +155,6 @@ import ConfigParser
from six import iteritems
from ansible.constants import get_config, mk_boolean
try:
import json
except ImportError:
@ -166,11 +164,12 @@ try:
import pyrax
from pyrax.utils import slugify
except ImportError:
print('pyrax is required for this module')
sys.exit(1)
sys.exit('pyrax is required for this module')
from time import time
from ansible.constants import get_config, mk_boolean
NON_CALLABLES = (basestring, bool, dict, int, list, type(None))
@ -227,12 +226,21 @@ def _list_into_cache(regions):
prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta')
networks = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK',
'public', islist=True)
try:
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
'RAX_ACCESS_IP_VERSION', 4,
islist=True))
# Ansible 2.3+
networks = get_config(p, 'rax', 'access_network',
'RAX_ACCESS_NETWORK', 'public', value_type='list')
except TypeError:
# Ansible 2.2.x and below
networks = get_config(p, 'rax', 'access_network',
'RAX_ACCESS_NETWORK', 'public', islist=True)
try:
try:
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
'RAX_ACCESS_IP_VERSION', 4, value_type='list'))
except TypeError:
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
'RAX_ACCESS_IP_VERSION', 4, islist=True))
except:
ip_versions = [4]
else:
@ -406,10 +414,9 @@ def setup():
if os.path.isfile(default_creds_file):
creds_file = default_creds_file
elif not keyring_username:
sys.stderr.write('No value in environment variable %s and/or no '
'credentials file at %s\n'
% ('RAX_CREDS_FILE', default_creds_file))
sys.exit(1)
sys.exit('No value in environment variable %s and/or no '
'credentials file at %s'
% ('RAX_CREDS_FILE', default_creds_file))
identity_type = pyrax.get_setting('identity_type')
pyrax.set_setting('identity_type', identity_type or 'rackspace')
@ -422,23 +429,28 @@ def setup():
else:
pyrax.set_credential_file(creds_file, region=region)
except Exception as e:
sys.stderr.write("%s: %s\n" % (e, e.message))
sys.exit(1)
sys.exit("%s: %s" % (e, e.message))
regions = []
if region:
regions.append(region)
else:
region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all',
islist=True)
try:
# Ansible 2.3+
region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all',
value_type='list')
except TypeError:
# Ansible 2.2.x and below
region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all',
islist=True)
for region in region_list:
region = region.strip().upper()
if region == 'ALL':
regions = pyrax.regions
break
elif region not in pyrax.regions:
sys.stderr.write('Unsupported region %s' % region)
sys.exit(1)
sys.exit('Unsupported region %s' % region)
elif region not in regions:
regions.append(region)

View File

@ -494,6 +494,9 @@ AWX_TASK_ENV = {}
# before it recycles
JOB_EVENT_RECYCLE_THRESHOLD = 3000
# Number of workers used to proecess job events in parallel
JOB_EVENT_WORKERS = 4
# Maximum number of job events that can be waiting on a single worker queue before
# it can be skipped as too busy
JOB_EVENT_MAX_QUEUE_SIZE = 100
@ -592,6 +595,7 @@ INV_ENV_VARIABLE_BLACKLIST = ("HOME", "USER", "_", "TERM")
# http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region
EC2_REGION_NAMES = {
'us-east-1': 'US East (Northern Virginia)',
'us-east-2': 'US East (Ohio)',
'us-west-2': 'US West (Oregon)',
'us-west-1': 'US West (Northern California)',
'eu-central-1': 'EU (Frankfurt)',
@ -600,6 +604,7 @@ EC2_REGION_NAMES = {
'ap-southeast-2': 'Asia Pacific (Sydney)',
'ap-northeast-1': 'Asia Pacific (Tokyo)',
'ap-northeast-2': 'Asia Pacific (Seoul)',
'ap-south-1': 'Asia Pacific (Mumbai)',
'sa-east-1': 'South America (Sao Paulo)',
'us-gov-west-1': 'US West (GovCloud)',
'cn-north-1': 'China (Beijing)',
@ -739,7 +744,7 @@ OPENSTACK_INSTANCE_ID_VAR = 'openstack.id'
# ----- Foreman -----
# ---------------------
SATELLITE6_ENABLED_VAR = 'foreman.enabled'
SATELLITE6_ENABLED_VALUE = 'true'
SATELLITE6_ENABLED_VALUE = 'True'
SATELLITE6_GROUP_FILTER = r'^.+$'
SATELLITE6_HOST_FILTER = r'^.+$'
SATELLITE6_EXCLUDE_EMPTY_GROUPS = True

View File

@ -438,6 +438,16 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
}
});
if ($scope.pathsReadyRemove) {
$scope.pathsReadyRemove();
}
$scope.pathsReadyRemove = $scope.$on('pathsReady', function () {
CreateSelect2({
element: '#local-path-select',
multiple: false
});
});
// After the project is loaded, retrieve each related set
if ($scope.projectLoadedRemove) {
$scope.projectLoadedRemove();
@ -455,6 +465,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
$scope.project_local_paths = opts;
$scope.local_path = $scope.project_local_paths[0];
$scope.base_dir = 'You do not have access to view this property';
$scope.$emit('pathsReady');
}
$scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false;
@ -524,11 +535,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
multiple: false
});
CreateSelect2({
element: '#local-path-select',
multiple: false
});
$scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch';
$scope.scm_update_tooltip = "Start an SCM update";
$scope.scm_type_class = "";

View File

@ -157,7 +157,7 @@ export default
open: false,
index: false,
actions: {},
emptyListText: 'This user is not a member of any teams',
fields: {
name: {
key: true,

View File

@ -80,6 +80,7 @@ export default
// trigger display of alert block when scm_type == manual
scope.showMissingPlaybooksAlert = true;
}
scope.$emit('pathsReady');
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',

View File

@ -5,14 +5,14 @@
*************************************************/
export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'ParseTypeChange', 'GenerateForm', 'inventoryData',
'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService',
function($state, $stateParams, $scope, GroupForm, CredentialList, ParseTypeChange, GenerateForm, inventoryData,
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService) {
var form = GroupForm();
'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService', 'ToJSON',
function($state, $stateParams, $scope, GroupForm, CredentialList, ParseTypeChange, GenerateForm, inventoryData,
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, ToJSON) {
var generator = GenerateForm,
form = GroupForm();
init();
function init() {
function init() {n
// apply form definition's default field values
GenerateForm.applyDefaults(form, $scope);
@ -46,9 +46,10 @@ export default ['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList
$scope.formSave = function() {
var params, source;
json_data = ToJSON($scope.parseType, $scope.variables, true);
// group fields
var group = {
variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
variables: json_data,
name: $scope.name,
description: $scope.description,
inventory: inventoryData.id

View File

@ -5,9 +5,9 @@
*************************************************/
export default ['$state', '$stateParams', '$scope', 'ToggleNotification', 'ParseVariableString',
'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData',
function($state, $stateParams, $scope, ToggleNotification, ParseVariableString,
ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData) {
'ParseTypeChange', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', 'ToJSON',
function($state, $stateParams, $scope, ToggleNotification, ParseVariableString,
ParseTypeChange, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData, ToJSON) {
init();
@ -58,9 +58,10 @@ export default ['$state', '$stateParams', '$scope', 'ToggleNotification', 'Parse
};
$scope.formSave = function() {
var params, source;
json_data = ToJSON($scope.parseType, $scope.variables, true);
// group fields
var group = {
variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
variables: json_data,
name: $scope.name,
description: $scope.description,
inventory: $scope.inventory,

View File

@ -5,9 +5,9 @@
*************************************************/
export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange',
'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath',
function($state, $stateParams, $scope, HostForm, ParseTypeChange,
GenerateForm, HostManageService, rbacUiControlService, GetBasePath) {
'GenerateForm', 'HostManageService', 'rbacUiControlService', 'GetBasePath', 'ToJSON',
function($state, $stateParams, $scope, HostForm, ParseTypeChange,
GenerateForm, HostManageService, rbacUiControlService, GetBasePath, ToJSON) {
init();
@ -36,9 +36,10 @@ export default ['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange
$scope.toggleHostEnabled = function() {
$scope.host.enabled = !$scope.host.enabled;
};
$scope.formSave = function() {
var params = {
variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
$scope.formSave = function(){
var json_data = ToJSON($scope.parseType, $scope.variables, true),
params = {
variables: json_data,// $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
name: $scope.name,
description: $scope.description,
enabled: $scope.host.enabled,

View File

@ -5,8 +5,8 @@
*************************************************/
export default
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'HostManageService', 'host',
function($state, $stateParams, $scope, HostForm, ParseTypeChange, HostManageService, host){
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'HostManageService', 'host', 'ToJSON',
function($state, $stateParams, $scope, HostForm, ParseTypeChange, HostManageService, host, ToJSON){
init();
@ -34,9 +34,10 @@
$scope.host.enabled = !$scope.host.enabled;
};
$scope.formSave = function(){
var host = {
var json_data = ToJSON($scope.parseType, $scope.variables, true),
host = {
id: $scope.host.id,
variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
variables: json_data,
name: $scope.name,
description: $scope.description,
enabled: $scope.host.enabled

View File

@ -125,6 +125,8 @@
.OnePlusTwo-left--detailsRow;
}
.HostEvent-field--content{
word-wrap: break-word;
max-width: 13em;
flex: 0 1 13em;
}
.HostEvent-details--left, .HostEvent-details--right{
@ -138,6 +140,7 @@
flex: 0 1 25em;
}
.HostEvent-field--content{
max-width: 15em;
flex: 0 1 15em;
align-self: flex-end;
}

View File

@ -130,12 +130,20 @@ export default
goToJobDetails('managementJobStdout');
}
else if(_.has(data, 'project_update')) {
if($state.current.name !== 'projects') {
// If we are on the projects list or any child state of that list
// then we want to stay on that page. Otherwise go to the stdout
// view.
if(!$state.includes('projects')) {
goToJobDetails('scmUpdateStdout');
}
}
else if(_.has(data, 'inventory_update')) {
goToJobDetails('inventorySyncStdout');
// If we are on the inventory manage page or any child state of that
// page then we want to stay on that page. Otherwise go to the stdout
// view.
if(!$state.includes('inventoryManage')) {
goToJobDetails('inventorySyncStdout');
}
}
}
if(scope.clearDialog) {

View File

@ -5,39 +5,56 @@
*************************************************/
export default
['$rootScope', 'Rest', 'ProcessErrors', 'GetBasePath', 'moment',
function($rootScope, Rest, ProcessErrors, GetBasePath, moment){
return {
get: function(id){
var defaultUrl = GetBasePath('job_templates') + '?id=' + id;
Rest.setUrl(defaultUrl);
return Rest.get()
.success(function(res){
return res;
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
set: function(data){
var defaultUrl = GetBasePath('job_templates');
Rest.setUrl(defaultUrl);
var name = this.buildName(data.results[0].name);
data.results[0].name = name + ' @ ' + moment().format('h:mm:ss a'); // 2:49:11 pm
return Rest.post(data.results[0])
.success(function(res){
return res;
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
buildName: function(name){
var result = name.split('@')[0];
return result;
}
};
}
];
['$rootScope', 'Rest', 'ProcessErrors', 'GetBasePath', 'moment',
function($rootScope, Rest, ProcessErrors, GetBasePath, moment){
return {
get: function(id){
var defaultUrl = GetBasePath('job_templates') + '?id=' + id;
Rest.setUrl(defaultUrl);
return Rest.get()
.success(function(res){
return res;
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
getSurvey: function(endpoint){
Rest.setUrl(endpoint);
return Rest.get();
},
copySurvey: function(source, target){
return this.getSurvey(source.related.survey_spec).success( (data) => {
Rest.setUrl(target.related.survey_spec);
return Rest.post(data);
});
},
set: function(data){
var defaultUrl = GetBasePath('job_templates');
var self = this;
Rest.setUrl(defaultUrl);
var name = this.buildName(data.results[0].name);
data.results[0].name = name + ' @ ' + moment().format('h:mm:ss a'); // 2:49:11 pm
return Rest.post(data.results[0])
.success(function(job_template_res){
// also copy any associated survey_spec
if (data.results[0].related.survey_spec){
return self.copySurvey(data.results[0], job_template_res).success( () => job_template_res);
}
else{
return job_template_res;
}
})
.error(function(res, status){
ProcessErrors($rootScope, res, status, null, {hdr: 'Error!',
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
});
},
buildName: function(name){
var result = name.split('@')[0];
return result;
}
};
}
];

View File

@ -13,6 +13,7 @@ export default
return {
setPendoOptions: function (config) {
var tower_version = config.version.split('-')[0],
trial = (config.trial) ? config.trial : false,
options = {
visitor: {
id: null,
@ -24,7 +25,7 @@ export default
planLevel: config.license_type,
planPrice: config.instance_count,
creationDate: config.license_date,
trial: config.trial,
trial: trial,
tower_version: tower_version,
ansible_version: config.ansible_version
}
@ -92,49 +93,18 @@ export default
return deferred.promise;
},
getConfig: function () {
var config = ConfigService.get(),
deferred = $q.defer();
if(_.isEmpty(config)){
var url = GetBasePath('config');
Rest.setUrl(url);
var promise = Rest.get();
promise.then(function (response) {
config = response.data.license_info;
config.analytics_status = response.data.analytics_status;
config.version = response.data.version;
config.ansible_version = response.data.ansible_version;
if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){
$pendolytics.bootstrap();
deferred.resolve(config);
}
else {
deferred.reject('Pendo is turned off.');
}
});
promise.catch(function (response) {
ProcessErrors($rootScope, response.data, response.status, null, {
hdr: 'Error!',
msg: 'Failed to get inventory name. GET returned status: ' +
response.status });
deferred.reject('Could not resolve pendo config.');
});
}
else if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){
$pendolytics.bootstrap();
deferred.resolve(config);
}
else {
deferred.reject('Pendo is turned off.');
}
return deferred.promise;
},
issuePendoIdentity: function () {
var that = this;
this.getConfig().then(function(config){
var options = that.setPendoOptions(config);
that.setRole(options).then(function(options){
var config,
options,
c = ConfigService.get(),
config = c.license_info;
config.analytics_status = c.analytics_status;
config.version = c.version;
config.ansible_version = c.ansible_version;
if(config.analytics_status === 'detailed' || config.analytics_status === 'anonymous'){
$pendolytics.bootstrap();
options = this.setPendoOptions(config);
this.setRole(options).then(function(options){
$log.debug('Pendo status is '+ config.analytics_status + '. Object below:');
$log.debug(options);
$pendolytics.identify(options);
@ -142,10 +112,10 @@ export default
// reject function for setRole
$log.debug(reason);
});
}, function(reason){
// reject function for getConfig
$log.debug(reason);
});
}
else {
$log.debug('Pendo is turned off.')
}
}
};
}

View File

@ -44,7 +44,6 @@ export default [{
value: {
order_by: 'username'
}
},
add_user_search: {
value: {
order_by: 'username',
@ -54,17 +53,15 @@ export default [{
squash: true
}
},
ncyBreadcrumb: {
parent: "organizations.edit",
label: "USERS"
},
data: {
activityStream: true,
activityStreamTarget: 'organization'
},
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "organizations.edit";
},
label: "USERS"
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
@ -129,10 +126,7 @@ export default [{
activityStreamTarget: 'organization'
},
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "organizations.edit";
},
parent: "organizations.edit",
label: "TEAMS"
},
resolve: {
@ -178,10 +172,7 @@ export default [{
activityStreamTarget: 'organization'
},
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "organizations.edit";
},
parent: "organizations.edit",
label: "INVENTORIES"
},
resolve: {
@ -232,10 +223,7 @@ export default [{
},
},
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "organizations.edit";
},
parent: "organizations.edit",
label: "PROJECTS"
},
resolve: {
@ -293,10 +281,7 @@ export default [{
}
},
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "organizations.edit";
},
parent: "organizations.edit",
label: "JOB TEMPLATES"
},
resolve: {
@ -367,10 +352,7 @@ export default [{
activityStreamTarget: 'organization'
},
ncyBreadcrumb: {
parent: function($scope) {
$scope.$parent.$emit("ReloadOrgListView");
return "organizations.edit";
},
parent: "organizations.edit",
label: "ADMINS"
},
resolve: {

View File

@ -12,7 +12,6 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
$log, $compile, Rest, OrganizationList, Alert, Prompt, ClearScope,
ProcessErrors, GetBasePath, Wait, $state, rbacUiControlService, $filter, Dataset) {
ClearScope();
var defaultUrl = GetBasePath('organizations'),
@ -85,6 +84,27 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
});
return val;
});
};
$scope.$on("ReloadOrgListView", function() {
Rest.setUrl($scope.current_url);
Rest.get()
.success((data) => $scope.organizations = data.results)
.error(function(data, status) {
ProcessErrors($scope, data, status, null, {
hdr: 'Error!',
msg: 'Call to ' + defaultUrl + ' failed. DELETE returned status: ' + status
});
});
});
$scope.$watchCollection('organizations', function(value){
$scope.orgCards = parseCardData(value);
});
if ($scope.removePostRefresh) {
$scope.removePostRefresh();
}
$scope.$watchCollection(`${list.iterator}_dataset`, function(data) {
@ -129,5 +149,50 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
actionText: 'DELETE'
});
};
var init = function(){
// Pagination depends on html appended by list generator
view.inject(list, {
id: 'organizations-list',
scope: $scope,
mode: 'edit'
});
// grab the pagination elements, move, destroy list generator elements
$('#organization-pagination').appendTo('#OrgCards');
$('#organizations tag-search').appendTo('.OrgCards-search');
$('#organizations-list').remove();
PaginateInit({
scope: $scope,
list: list,
url: defaultUrl,
pageSize: pageSize,
});
SearchInit({
scope: $scope,
list: list,
url: defaultUrl,
set: 'organizations'
});
$scope.list = list;
$rootScope.flashMessage = null;
$scope.search(list.iterator);
var getOrgCount = function() {
Rest.setUrl(defaultUrl);
Rest.get()
.success(function(data) {
$scope.orgCount = data.count;
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, {
hdr: 'Error!',
msg: 'Call to ' + defaultUrl + ' failed. DELETE returned status: ' + status
});
});
};
getOrgCount();
};
init();
}
];

View File

@ -2,11 +2,11 @@ git+https://github.com/chrismeyersfsu/ansiconv.git@tower_1.0.0#egg=ansiconv
amqp==1.4.9
anyjson==0.3.3
appdirs==1.4.0
azure==2.0.0rc2
azure==2.0.0rc5
Babel==2.2.0
baron==0.6.2
billiard==3.3.0.16
boto==2.40.0
boto==2.43.0
celery==3.1.23
cliff==1.15.0
cmd2==0.6.8
@ -116,7 +116,7 @@ rax-default-network-flags-python-novaclient-ext==0.3.2
rax-scheduled-images-python-novaclient-ext==0.3.1
redbaron==0.6.1
requests-oauthlib==0.5.0
requests==2.9.1
requests==2.11.0
requestsexceptions==1.1.1
rply==0.7.4
shade==1.4.0

View File

@ -3,7 +3,7 @@ apache-libcloud==0.20.1
appdirs==1.4.0
azure==2.0.0rc5
Babel==2.2.0
boto==2.40.0
boto==2.43.0
cliff==1.15.0
cmd2==0.6.8
cryptography==1.3.2