mirror of
https://github.com/ansible/awx.git
synced 2026-01-20 14:11:24 -03:30
Merge branch 'release_3.0.3' into stable
* release_3.0.3: (48 commits) 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 Switch pg user auth from trust to peer Remove support@ansible.com, clean up some old links. filter internal User.admin_roles from the /roles API list view Exclude test directory from setup bundle tarballs fixed manual project select2 population YAML parsing groups and hosts extra variables ...
This commit is contained in:
commit
0f651c8903
8
Makefile
8
Makefile
@ -23,7 +23,7 @@ CLIENT_TEST_DIR ?= build_test
|
||||
|
||||
# Python packages to install only from source (not from binary wheels)
|
||||
# Comma separated list
|
||||
SRC_ONLY_PKGS ?= cffi
|
||||
SRC_ONLY_PKGS ?= cffi,pycparser
|
||||
|
||||
# Determine appropriate shasum command
|
||||
UNAME_S := $(shell uname -s)
|
||||
@ -663,10 +663,10 @@ release_build:
|
||||
# Build setup tarball
|
||||
tar-build/$(SETUP_TAR_FILE):
|
||||
@mkdir -p tar-build
|
||||
@cp -a setup tar-build/$(SETUP_TAR_NAME)
|
||||
@rsync -az --exclude /test setup/ tar-build/$(SETUP_TAR_NAME)
|
||||
@rsync -az docs/licenses tar-build/$(SETUP_TAR_NAME)/
|
||||
@cd tar-build/$(SETUP_TAR_NAME) && sed -e 's#%NAME%#$(NAME)#;s#%VERSION%#$(VERSION)#;s#%RELEASE%#$(RELEASE)#;' group_vars/all.in > group_vars/all
|
||||
@cd tar-build && tar -czf $(SETUP_TAR_FILE) --exclude "*/all.in" --exclude "**/test/*" $(SETUP_TAR_NAME)/
|
||||
@cd tar-build && tar -czf $(SETUP_TAR_FILE) --exclude "*/all.in" $(SETUP_TAR_NAME)/
|
||||
@ln -sf $(SETUP_TAR_FILE) tar-build/$(SETUP_TAR_LINK)
|
||||
|
||||
tar-build/$(SETUP_TAR_CHECKSUM):
|
||||
@ -703,7 +703,7 @@ setup-bundle-build:
|
||||
|
||||
# TODO - Somehow share implementation with setup_tarball
|
||||
setup-bundle-build/$(OFFLINE_TAR_FILE):
|
||||
cp -a setup setup-bundle-build/$(OFFLINE_TAR_NAME)
|
||||
rsync -az --exclude /test setup/ setup-bundle-build/$(OFFLINE_TAR_NAME)
|
||||
rsync -az docs/licenses setup-bundle-build/$(OFFLINE_TAR_NAME)/
|
||||
cd setup-bundle-build/$(OFFLINE_TAR_NAME) && sed -e 's#%NAME%#$(NAME)#;s#%VERSION%#$(VERSION)#;s#%RELEASE%#$(RELEASE)#;' group_vars/all.in > group_vars/all
|
||||
$(PYTHON) $(DEPS_SCRIPT) -d $(DIST) -r $(DIST_MAJOR) -u $(AW_REPO_URL) -s setup-bundle-build/$(OFFLINE_TAR_NAME) -v -v -v
|
||||
|
||||
@ -5,7 +5,7 @@ import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
__version__ = '3.0.2'
|
||||
__version__ = '3.0.3'
|
||||
|
||||
__all__ = ['__version__']
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -661,7 +661,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):
|
||||
@ -1095,17 +1094,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):
|
||||
@ -1123,8 +1137,10 @@ class SystemJobTemplateAccess(BaseAccess):
|
||||
|
||||
model = SystemJobTemplate
|
||||
|
||||
@check_superuser
|
||||
def can_start(self, obj):
|
||||
return self.can_read(obj)
|
||||
'''Only a superuser can start a job from a SystemJobTemplate'''
|
||||
return False
|
||||
|
||||
class SystemJobAccess(BaseAccess):
|
||||
'''
|
||||
|
||||
@ -25,8 +25,6 @@ from awx.main.socket import Socket
|
||||
|
||||
logger = logging.getLogger('awx.main.commands.run_callback_receiver')
|
||||
|
||||
WORKERS = 4
|
||||
|
||||
class CallbackReceiver(object):
|
||||
def __init__(self):
|
||||
self.parent_mappings = {}
|
||||
@ -54,7 +52,7 @@ class CallbackReceiver(object):
|
||||
|
||||
if use_workers:
|
||||
connection.close()
|
||||
for idx in range(WORKERS):
|
||||
for idx in range(settings.JOB_EVENT_WORKERS):
|
||||
queue_actual = Queue(settings.JOB_EVENT_MAX_QUEUE_SIZE)
|
||||
w = Process(target=self.callback_worker, args=(queue_actual, idx,))
|
||||
w.start()
|
||||
@ -99,7 +97,7 @@ class CallbackReceiver(object):
|
||||
time.sleep(0.1)
|
||||
|
||||
def write_queue_worker(self, preferred_queue, worker_queues, message):
|
||||
queue_order = sorted(range(WORKERS), cmp=lambda x, y: -1 if x==preferred_queue else 0)
|
||||
queue_order = sorted(range(settings.JOB_EVENT_WORKERS), cmp=lambda x, y: -1 if x==preferred_queue else 0)
|
||||
for queue_actual in queue_order:
|
||||
try:
|
||||
worker_actual = worker_queues[queue_actual]
|
||||
@ -153,7 +151,7 @@ class CallbackReceiver(object):
|
||||
if message['event'] == 'playbook_on_stats':
|
||||
job_parent_events = {}
|
||||
|
||||
actual_queue = self.write_queue_worker(total_messages % WORKERS, worker_queues, message)
|
||||
actual_queue = self.write_queue_worker(total_messages % settings.JOB_EVENT_WORKERS, worker_queues, message)
|
||||
# NOTE: It might be better to recycle the entire callback receiver process if one or more of the queues are too full
|
||||
# the drawback is that if we under extremely high load we may be legitimately taking a while to process messages
|
||||
if actual_queue is None:
|
||||
@ -282,7 +280,6 @@ class CallbackReceiver(object):
|
||||
return None
|
||||
|
||||
def callback_worker(self, queue_actual, idx):
|
||||
messages_processed = 0
|
||||
while True:
|
||||
try:
|
||||
message = queue_actual.get(block=True, timeout=1)
|
||||
@ -292,10 +289,6 @@ class CallbackReceiver(object):
|
||||
logger.error("Exception on listen socket, restarting: " + str(e))
|
||||
break
|
||||
self.process_job_event(message)
|
||||
messages_processed += 1
|
||||
if messages_processed >= settings.JOB_EVENT_RECYCLE_THRESHOLD:
|
||||
logger.info("Shutting down message receiver")
|
||||
break
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
'''
|
||||
|
||||
23
awx/main/migrations/0033_v303_v245_host_variable_fix.py
Normal file
23
awx/main/migrations/0033_v303_v245_host_variable_fix.py
Normal 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),
|
||||
]
|
||||
@ -575,7 +575,7 @@ class BaseTask(Task):
|
||||
instance = self.update_model(instance.pk, status='running',
|
||||
output_replacements=output_replacements)
|
||||
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():
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -8,8 +8,16 @@ from awx.main.access import (
|
||||
BaseAccess,
|
||||
check_superuser,
|
||||
JobTemplateAccess,
|
||||
SystemJobTemplateAccess,
|
||||
)
|
||||
|
||||
from awx.main.models import (
|
||||
Credential,
|
||||
Inventory,
|
||||
Project,
|
||||
Role,
|
||||
Organization,
|
||||
)
|
||||
from awx.main.models import Credential, Inventory, Project, Role, Organization
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -110,3 +118,12 @@ def test_jt_can_add_bad_data(user_unit):
|
||||
access = JobTemplateAccess(user_unit)
|
||||
assert not access.can_add({'asdf': 'asdf'})
|
||||
|
||||
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)
|
||||
|
||||
11
awx/main/tests/unit/test_settings.py
Normal file
11
awx/main/tests/unit/test_settings.py
Normal 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']
|
||||
@ -38,7 +38,7 @@ import os
|
||||
import pwd
|
||||
import urlparse
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from copy import copy
|
||||
|
||||
# Requests
|
||||
import requests
|
||||
@ -228,7 +228,7 @@ class BaseCallbackModule(object):
|
||||
|
||||
def _log_event(self, event, **event_data):
|
||||
if 'res' in event_data:
|
||||
event_data['res'] = censor(deepcopy(event_data['res']))
|
||||
event_data['res'] = censor(copy(event_data['res']))
|
||||
|
||||
if self.callback_consumer_port:
|
||||
self._post_job_event_queue_msg(event, event_data)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -438,6 +438,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
|
||||
@ -529,6 +532,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)',
|
||||
@ -537,6 +541,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)',
|
||||
@ -676,7 +681,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
|
||||
|
||||
@ -31,4 +31,4 @@ if not all([SOCIAL_AUTH_SAML_SP_ENTITY_ID, SOCIAL_AUTH_SAML_SP_PUBLIC_CERT,
|
||||
AUTHENTICATION_BACKENDS = [x for x in AUTHENTICATION_BACKENDS if x != 'awx.sso.backends.SAMLAuth']
|
||||
|
||||
if not AUTH_BASIC_ENABLED:
|
||||
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'rest_framework.authentication.BasicAuthentication']
|
||||
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [x for x in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] if x != 'awx.api.authentication.LoggedBasicAuthentication']
|
||||
|
||||
@ -8,7 +8,7 @@ import threading
|
||||
xmlsec_init_lock = threading.Lock()
|
||||
xmlsec_initialized = False
|
||||
|
||||
import dm.xmlsec.binding
|
||||
import dm.xmlsec.binding # noqa
|
||||
original_xmlsec_initialize = dm.xmlsec.binding.initialize
|
||||
|
||||
def xmlsec_initialize(*args, **kwargs):
|
||||
|
||||
@ -596,6 +596,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();
|
||||
@ -623,6 +633,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');
|
||||
}
|
||||
|
||||
LookUpInit({
|
||||
@ -718,11 +729,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';
|
||||
|
||||
// Initialize related search functions. Doing it here to make sure relatedSets object is populated.
|
||||
|
||||
@ -141,7 +141,7 @@ export default
|
||||
open: false,
|
||||
index: false,
|
||||
actions: {},
|
||||
|
||||
emptyListText: 'This user is not a member of any teams',
|
||||
fields: {
|
||||
name: {
|
||||
key: true,
|
||||
|
||||
@ -78,6 +78,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!',
|
||||
|
||||
@ -71,7 +71,27 @@ export default
|
||||
|
||||
// if you're editing an object, make sure you're on the right
|
||||
// page to display the element you are editing
|
||||
if (scope.addedItem) {
|
||||
if (params.fromSearch) {
|
||||
var url = params.url;
|
||||
// for a search, we want to make sure to get the first page of
|
||||
// results
|
||||
if (url.indexOf("page=") > -1) {
|
||||
// if the url includes a page, remove that part
|
||||
var urlArr = url.split("page=");
|
||||
var afterPageUrlArr = urlArr[1].split("&");
|
||||
|
||||
if (afterPageUrlArr.length > 1) {
|
||||
// if there's stuff after the page part,
|
||||
// put that back in
|
||||
afterPageUrlArr.shift();
|
||||
url = urlArr[0] +
|
||||
afterPageUrlArr.join("&");
|
||||
} else {
|
||||
url = urlArr[0];
|
||||
}
|
||||
}
|
||||
getPage(url);
|
||||
} else if (scope.addedItem) {
|
||||
id = scope.addedItem + "";
|
||||
delete scope.addedItem;
|
||||
$rootScope.rowBeingEdited = id;
|
||||
|
||||
@ -5,10 +5,8 @@
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'inventoryScriptsListObject', 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'LookUpInit',
|
||||
'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions',
|
||||
function($state, $stateParams, $scope, GroupForm, CredentialList, InventoryScriptsList, ParseTypeChange, GenerateForm, inventoryData, LookUpInit,
|
||||
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions){
|
||||
['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'inventoryScriptsListObject', 'ParseTypeChange', 'GenerateForm', 'inventoryData', 'LookUpInit', 'GroupManageService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions','ToJSON',
|
||||
function($state, $stateParams, $scope, GroupForm, CredentialList, InventoryScriptsList, ParseTypeChange, GenerateForm, inventoryData, LookUpInit, GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, ToJSON){
|
||||
var generator = GenerateForm,
|
||||
form = GroupForm();
|
||||
|
||||
@ -20,10 +18,11 @@
|
||||
$state.go('^');
|
||||
};
|
||||
$scope.formSave = function(){
|
||||
var params, source;
|
||||
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
|
||||
|
||||
@ -7,10 +7,10 @@
|
||||
export default
|
||||
['$state', '$stateParams', '$scope', 'GroupForm', 'CredentialList', 'inventoryScriptsListObject', 'ToggleNotification', 'ParseVariableString',
|
||||
'ParseTypeChange', 'GenerateForm', 'LookUpInit', 'RelatedSearchInit', 'RelatedPaginateInit', 'NotificationsListInit',
|
||||
'GroupManageService','GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData',
|
||||
'GroupManageService','GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'groupData', 'inventorySourceData', 'ToJSON',
|
||||
function($state, $stateParams, $scope, GroupForm, CredentialList, InventoryScriptsList, ToggleNotification, ParseVariableString,
|
||||
ParseTypeChange, GenerateForm, LookUpInit, RelatedSearchInit, RelatedPaginateInit, NotificationsListInit,
|
||||
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData){
|
||||
GroupManageService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, groupData, inventorySourceData, ToJSON){
|
||||
var generator = GenerateForm,
|
||||
form = GroupForm();
|
||||
|
||||
@ -22,15 +22,18 @@
|
||||
$state.go('^');
|
||||
};
|
||||
$scope.formSave = function(){
|
||||
var params, source;
|
||||
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,
|
||||
id: groupData.id
|
||||
};
|
||||
|
||||
if ($scope.source){
|
||||
// inventory_source fields
|
||||
params = {
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService',
|
||||
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService){
|
||||
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'ToJSON',
|
||||
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, ToJSON){
|
||||
var generator = GenerateForm,
|
||||
form = HostForm;
|
||||
$scope.parseType = 'yaml';
|
||||
@ -17,8 +17,9 @@
|
||||
$scope.host.enabled = !$scope.host.enabled;
|
||||
};
|
||||
$scope.formSave = function(){
|
||||
var params = {
|
||||
variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
|
||||
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,
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
*************************************************/
|
||||
|
||||
export default
|
||||
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'host',
|
||||
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, host){
|
||||
['$state', '$stateParams', '$scope', 'HostForm', 'ParseTypeChange', 'GenerateForm', 'HostManageService', 'host', 'ParseVariableString', 'ToJSON',
|
||||
function($state, $stateParams, $scope, HostForm, ParseTypeChange, GenerateForm, HostManageService, host, ParseVariableString, ToJSON){
|
||||
var generator = GenerateForm,
|
||||
form = HostForm;
|
||||
$scope.parseType = 'yaml';
|
||||
@ -17,9 +17,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
|
||||
@ -31,12 +32,12 @@
|
||||
var init = function(){
|
||||
$scope.host = host;
|
||||
generator.inject(form, {mode: 'edit', related: false, id: 'Inventory-hostManage--panel', scope: $scope});
|
||||
$scope.variables = host.variables === '' ? '---' : host.variables;
|
||||
$scope.variables = host.variables === '' ? '---' : ParseVariableString(host.variables);
|
||||
$scope.name = host.name;
|
||||
$scope.description = host.description;
|
||||
ParseTypeChange({
|
||||
scope: $scope,
|
||||
field_id: 'host_variables',
|
||||
field_id: 'host_variables'
|
||||
});
|
||||
};
|
||||
init();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@ -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.')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -23,10 +23,7 @@ export default [
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "organizations.edit";
|
||||
},
|
||||
parent: "organizations.edit",
|
||||
label: "USERS"
|
||||
},
|
||||
resolve: {
|
||||
@ -45,10 +42,7 @@ export default [
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "organizations.edit";
|
||||
},
|
||||
parent: "organizations.edit",
|
||||
label: "TEAMS"
|
||||
},
|
||||
resolve: {
|
||||
@ -67,10 +61,7 @@ export default [
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "organizations.edit";
|
||||
},
|
||||
parent: "organizations.edit",
|
||||
label: "INVENTORIES"
|
||||
},
|
||||
resolve: {
|
||||
@ -89,10 +80,7 @@ export default [
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "organizations.edit";
|
||||
},
|
||||
parent: "organizations.edit",
|
||||
label: "PROJECTS"
|
||||
},
|
||||
resolve: {
|
||||
@ -111,10 +99,7 @@ export default [
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "organizations.edit";
|
||||
},
|
||||
parent: "organizations.edit",
|
||||
label: "JOB TEMPLATES"
|
||||
},
|
||||
resolve: {
|
||||
@ -133,10 +118,7 @@ export default [
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "organizations.edit";
|
||||
},
|
||||
parent: "organizations.edit",
|
||||
label: "ADMINS"
|
||||
},
|
||||
resolve: {
|
||||
|
||||
@ -8,12 +8,12 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
|
||||
'$log', '$compile', 'Rest', 'PaginateInit',
|
||||
'SearchInit', 'OrganizationList', 'Alert', 'Prompt', 'ClearScope',
|
||||
'ProcessErrors', 'GetBasePath', 'Wait',
|
||||
'$state', 'generateList', 'Refresh', '$filter',
|
||||
'$state', 'generateList', '$filter',
|
||||
function($stateParams, $scope, $rootScope, $location,
|
||||
$log, $compile, Rest, PaginateInit,
|
||||
SearchInit, OrganizationList, Alert, Prompt, ClearScope,
|
||||
ProcessErrors, GetBasePath, Wait,
|
||||
$state, generateList, Refresh, $filter) {
|
||||
$state, generateList, $filter) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -70,19 +70,14 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
|
||||
};
|
||||
|
||||
$scope.$on("ReloadOrgListView", function() {
|
||||
var url = GetBasePath('organizations') + '?';
|
||||
if ($state.$current.self.name === "organizations" ||
|
||||
$state.$current.self.name === "organizations.add") {
|
||||
$scope.activeCard = null;
|
||||
}
|
||||
if ($scope[list.iterator + 'SearchFilters']){
|
||||
url = url + _.reduce($scope[list.iterator+'SearchFilters'], (result, filter) => result + '&' + filter.url, '');
|
||||
}
|
||||
Refresh({
|
||||
scope: $scope,
|
||||
set: list.name,
|
||||
iterator: list.iterator,
|
||||
url: url
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -158,7 +153,7 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
|
||||
});
|
||||
// grab the pagination elements, move, destroy list generator elements
|
||||
$('#organization-pagination').appendTo('#OrgCards');
|
||||
$('tag-search').appendTo('.OrgCards-search');
|
||||
$('#organizations tag-search').appendTo('.OrgCards-search');
|
||||
$('#organizations-list').remove();
|
||||
|
||||
PaginateInit({
|
||||
|
||||
@ -17,10 +17,7 @@ export default {
|
||||
activityStreamTarget: 'organization'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: function($scope) {
|
||||
$scope.$parent.$emit("ReloadOrgListView");
|
||||
return "setup";
|
||||
},
|
||||
parent: "setup",
|
||||
label: "ORGANIZATIONS"
|
||||
}
|
||||
};
|
||||
|
||||
@ -72,7 +72,8 @@ export default ['$scope', 'Refresh', 'tagSearchService', '$stateParams',
|
||||
scope: listScope,
|
||||
set: set,
|
||||
iterator: iterator,
|
||||
url: url
|
||||
url: url,
|
||||
fromSearch: true
|
||||
});
|
||||
|
||||
listScope.$on('PostRefresh', function() {
|
||||
|
||||
@ -2,10 +2,10 @@ git+https://github.com/chrismeyersfsu/ansiconv.git@tower_1.0.0#egg=ansiconv
|
||||
amqp==1.4.5
|
||||
anyjson==0.3.3
|
||||
appdirs==1.4.0
|
||||
azure==2.0.0rc2
|
||||
azure==2.0.0rc5
|
||||
Babel==2.2.0
|
||||
billiard==3.3.0.16
|
||||
boto==2.40.0
|
||||
boto==2.43.0
|
||||
celery==3.1.10
|
||||
cliff==1.15.0
|
||||
cmd2==0.6.8
|
||||
@ -113,7 +113,7 @@ rax-default-network-flags-python-novaclient-ext==0.3.2
|
||||
rax-scheduled-images-python-novaclient-ext==0.3.1
|
||||
redis==2.10.3
|
||||
requests-oauthlib==0.5.0
|
||||
requests==2.9.1
|
||||
requests==2.11.0
|
||||
requestsexceptions==1.1.1
|
||||
shade==1.4.0
|
||||
simplejson==3.8.1
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
anyjson==0.3.3
|
||||
apache-libcloud==0.20.1
|
||||
appdirs==1.4.0
|
||||
azure==2.0.0rc2
|
||||
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
|
||||
@ -69,7 +69,7 @@ rackspace-auth-openstack==1.3
|
||||
rackspace-novaclient==1.5
|
||||
rax-default-network-flags-python-novaclient-ext==0.3.2
|
||||
rax-scheduled-images-python-novaclient-ext==0.3.1
|
||||
requests==2.5.1
|
||||
requests==2.11.0
|
||||
requestsexceptions==1.1.1
|
||||
shade==1.4.0
|
||||
simplejson==3.8.1
|
||||
|
||||
6
setup.py
6
setup.py
@ -79,13 +79,13 @@ setup(
|
||||
name='ansible-tower',
|
||||
version=__version__.split("-")[0], # FIXME: Should keep full version here?
|
||||
author='Ansible, Inc.',
|
||||
author_email='support@ansible.com',
|
||||
author_email='info@ansible.com',
|
||||
description='ansible-tower: API, UI and Task Engine for Ansible',
|
||||
long_description='AWX provides a web-based user interface, REST API and '
|
||||
long_description='Ansible Tower provides a web-based user interface, REST API and '
|
||||
'task engine built on top of Ansible',
|
||||
license='Proprietary',
|
||||
keywords='ansible',
|
||||
url='http://github.com/ansible/ansible-commander',
|
||||
url='http://github.com/ansible/ansible-tower',
|
||||
packages=['awx'],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
|
||||
@ -2,11 +2,11 @@ Metadata-Version: 1.1
|
||||
Name: ansible-tower
|
||||
Version: 3.0.0-0.devel
|
||||
Summary: ansible-tower: API, UI and Task Engine for Ansible
|
||||
Home-page: http://github.com/ansible/ansible-commander
|
||||
Home-page: http://github.com/ansible/ansible-tower
|
||||
Author: Ansible, Inc.
|
||||
Author-email: support@ansible.com
|
||||
Author-email: info@ansible.com
|
||||
License: Proprietary
|
||||
Description: AWX provides a web-based user interface, REST API and task engine built on top of Ansible
|
||||
Description: Ansible Tower provides a web-based user interface, REST API and task engine built on top of Ansible
|
||||
Keywords: ansible
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user