Merge branch 'release_3.1.2' into devel

* release_3.1.2: (33 commits)
  updating changelog for 3.1.2
  Add back SRC_ONLY_PKGS
  Fix ubuntu 14 restart service list pt 2
  Make sure the insight playbook fetch doesn't quote user and pass
  Add requirements/vendor to gitignore
  Conditionally install from local python dependencies in spec file
  Remove requirements/vendor on make clean.
  Update brew-srpm target to generate local requirements files
  Get offline pip installs working
  Add clean-dist target
  Navigate back to the jobDetails state when the user clicks outside the host event modal.
  Don't use jinja quote filter on insights username or password
  fix legacy standard out
  Fixed permissions typo
  add test, restore old behavior in api test
  create _survey_element_validation and use it for updating extra_vars
  Do not set the default if the field was not passed in to kwargs_extra_vars
  Remove log aggregator port required mark.
  Modify according to review feedback.
  Host Event json should be read-only
  ...
This commit is contained in:
Matthew Jones
2017-03-31 10:36:02 -04:00
26 changed files with 240 additions and 111 deletions

1
.gitignore vendored
View File

@@ -108,6 +108,7 @@ reports
*.results
local/
*.mo
requirements/vendor
# AWX python libs populated by requirements.txt
awx/lib/.deps_built

View File

@@ -225,15 +225,18 @@ clean-tmp:
clean-venv:
rm -rf venv/
clean-dist:
rm -rf dist
# Remove temporary build files, compiled Python files.
clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle
clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle clean-dist
rm -rf awx/public
rm -rf awx/lib/site-packages
rm -rf dist/*
rm -rf awx/job_status
rm -rf awx/job_output
rm -rf reports
rm -f awx/awx_test.sqlite3
rm -rf requirements/vendor
rm -rf tmp
mkdir tmp
rm -rf build $(NAME)-$(VERSION) *.egg-info
@@ -282,7 +285,11 @@ virtualenv_tower:
fi
requirements_ansible: virtualenv_ansible
$(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt
if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \
cat requirements/requirements_ansible.txt requirements/requirements_ansible_local.txt | $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed -r /dev/stdin ; \
else \
cat requirements/requirements_ansible.txt requirements/requirements_ansible_git.txt | $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --no-binary $(SRC_ONLY_PKGS) --ignore-installed -r /dev/stdin ; \
fi
$(VENV_BASE)/ansible/bin/pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt
requirements_ansible_dev:
@@ -292,7 +299,11 @@ requirements_ansible_dev:
# Install third-party requirements needed for Tower's environment.
requirements_tower: virtualenv_tower
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt
if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \
cat requirements/requirements.txt requirements/requirements_local.txt | $(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed -r /dev/stdin ; \
else \
cat requirements/requirements.txt requirements/requirements_git.txt | $(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --no-binary $(SRC_ONLY_PKGS) --ignore-installed -r /dev/stdin ; \
fi
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_tower_uninstall.txt
requirements_tower_dev:
@@ -690,39 +701,44 @@ setup_bundle_tarball: setup-bundle-build setup-bundle-build/$(OFFLINE_TAR_FILE)
rpm-build:
mkdir -p $@
rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE)
rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE) tar-build/$(SETUP_TAR_FILE)
cp packaging/rpm/$(NAME).spec rpm-build/
cp packaging/rpm/tower.te rpm-build/
cp packaging/rpm/tower.fc rpm-build/
cp packaging/rpm/$(NAME).sysconfig rpm-build/
cp packaging/remove_tower_source.py rpm-build/
cp packaging/bytecompile.sh rpm-build/
cp tar-build/$(SETUP_TAR_FILE) rpm-build/
if [ "$(OFFICIAL)" != "yes" ] ; then \
(cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \
(cd dist/ && mv $(NAME)-$(VERSION)-$(BUILD) $(NAME)-$(VERSION)) ; \
(cd dist/ && tar czf ../rpm-build/$(SDIST_TAR_FILE) $(NAME)-$(VERSION)) ; \
ln -sf $(SDIST_TAR_FILE) rpm-build/$(NAME)-$(VERSION).tar.gz ; \
(cd tar-build/ && tar zxf $(SETUP_TAR_FILE)) ; \
(cd tar-build/ && mv $(NAME)-setup-$(VERSION)-$(BUILD) $(NAME)-setup-$(VERSION)) ; \
(cd tar-build/ && tar czf ../rpm-build/$(SETUP_TAR_FILE) $(NAME)-setup-$(VERSION)) ; \
ln -sf $(SETUP_TAR_FILE) rpm-build/$(NAME)-setup-$(VERSION).tar.gz ; \
else \
cp -a dist/$(SDIST_TAR_FILE) rpm-build/ ; \
fi
rpmtar: sdist rpm-build/$(SDIST_TAR_FILE)
brewrpmtar: rpm-build/python-deps.tar.gz rpmtar
brewrpmtar: rpm-build/python-deps.tar.gz requirements/requirements_local.txt requirements/requirements_ansible_local.txt rpmtar
rpm-build/python-deps.tar.gz: requirements/vendor rpm-build
tar czf rpm-build/python-deps.tar.gz requirements/vendor
requirements/vendor:
pip download \
cat requirements/requirements.txt requirements/requirements_git.txt | pip download \
--no-binary=:all: \
--requirement=requirements/requirements_ansible.txt \
--requirement=/dev/stdin \
--dest=$@ \
--exists-action=i
pip download \
cat requirements/requirements_ansible.txt requirements/requirements_ansible_git.txt | pip download \
--no-binary=:all: \
--requirement=requirements/requirements.txt \
--requirement=/dev/stdin \
--dest=$@ \
--exists-action=i
@@ -732,6 +748,21 @@ requirements/vendor:
--dest=$@ \
--exists-action=i
requirements/requirements_local.txt:
@echo "This is going to take a while..."
pip download \
--requirement=requirements/requirements_git.txt \
--no-deps \
--exists-action=w \
--dest=requirements/vendor 2>/dev/null | sed -n 's/^\s*Saved\s*//p' > $@
requirements/requirements_ansible_local.txt:
pip download \
--requirement=requirements/requirements_ansible_git.txt \
--no-deps \
--exists-action=w \
--dest=requirements/vendor 2>/dev/null | sed -n 's/^\s*Saved\s*//p' > $@
rpm-build/$(RPM_NVR).src.rpm: /etc/mock/$(MOCK_CFG).cfg
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build \
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)

View File

@@ -60,7 +60,7 @@ def executor(tmpdir_factory, request):
cli = PlaybookCLI(['', 'playbook.yml'])
cli.parse()
options = cli.parser.parse_args([])[0]
options = cli.parser.parse_args(['-v'])[0]
loader = DataLoader()
variable_manager = VariableManager()
inventory = Inventory(loader=loader, variable_manager=variable_manager,

View File

@@ -30,6 +30,8 @@ from ansible.plugins.callback.default import CallbackModule as DefaultCallbackMo
from .events import event_context
from .minimal import CallbackModule as MinimalCallbackModule
CENSORED = "the output has been hidden due to the fact that 'no_log: true' was specified for this result" # noqa
class BaseCallbackModule(CallbackBase):
'''
@@ -69,8 +71,12 @@ class BaseCallbackModule(CallbackBase):
else:
task = None
if event_data.get('res') and event_data['res'].get('_ansible_no_log', False):
event_data['res'] = {'censored': "the output has been hidden due to the fact that 'no_log: true' was specified for this result"} # noqa
if event_data.get('res'):
if event_data['res'].get('_ansible_no_log', False):
event_data['res'] = {'censored': CENSORED}
for i, item in enumerate(event_data['res'].get('results', [])):
if event_data['res']['results'][i].get('_ansible_no_log', False):
event_data['res']['results'][i] = {'censored': CENSORED}
with event_context.display_lock:
try:

View File

@@ -242,9 +242,11 @@ register(
field_class=fields.IntegerField,
allow_null=True,
label=_('Logging Aggregator Port'),
help_text=_('Port on Logging Aggregator to send logs to (if required).'),
help_text=_('Port on Logging Aggregator to send logs to (if required and not'
' provided in Logging Aggregator).'),
category=_('Logging'),
category_slug='logging',
required=False
)
register(
'LOG_AGGREGATOR_TYPE',

View File

@@ -130,13 +130,18 @@ class SurveyJobTemplateMixin(models.Model):
for survey_element in self.survey_spec.get("spec", []):
default = survey_element.get('default')
variable_key = survey_element.get('variable')
if survey_element.get('type') == 'password':
if variable_key in kwargs_extra_vars and default:
kw_value = kwargs_extra_vars[variable_key]
if kw_value.startswith('$encrypted$') and kw_value != default:
kwargs_extra_vars[variable_key] = default
if default is not None:
extra_vars[variable_key] = default
data = {variable_key: default}
errors = self._survey_element_validation(survey_element, data)
if not errors:
extra_vars[variable_key] = default
# Overwrite job template extra vars with explicit job extra vars
# and add on job extra vars
@@ -144,6 +149,65 @@ class SurveyJobTemplateMixin(models.Model):
kwargs['extra_vars'] = json.dumps(extra_vars)
return kwargs
def _survey_element_validation(self, survey_element, data):
errors = []
if survey_element['variable'] not in data and survey_element['required']:
errors.append("'%s' value missing" % survey_element['variable'])
elif survey_element['type'] in ["textarea", "text", "password"]:
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (str, unicode):
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
survey_element['variable']))
return errors
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'integer':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != int:
errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']],
survey_element['variable']))
return errors
if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] < int(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'float':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (float, int):
errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']],
survey_element['variable']))
return errors
if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'multiselect':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != list:
errors.append("'%s' value is expected to be a list." % survey_element['variable'])
else:
for val in data[survey_element['variable']]:
if val not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'],
survey_element['choices']))
elif survey_element['type'] == 'multiplechoice':
if survey_element['variable'] in data:
if data[survey_element['variable']] not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']],
survey_element['variable'],
survey_element['choices']))
return errors
def survey_variable_validation(self, data):
errors = []
if not self.survey_enabled:
@@ -153,62 +217,7 @@ class SurveyJobTemplateMixin(models.Model):
if 'description' not in self.survey_spec:
errors.append("'description' missing from survey spec.")
for survey_element in self.survey_spec.get("spec", []):
if survey_element['variable'] not in data and \
survey_element['required']:
errors.append("'%s' value missing" % survey_element['variable'])
elif survey_element['type'] in ["textarea", "text", "password"]:
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (str, unicode):
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'integer':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != int:
errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] < int(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'float':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (float, int):
errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'multiselect':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != list:
errors.append("'%s' value is expected to be a list." % survey_element['variable'])
else:
for val in data[survey_element['variable']]:
if val not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'],
survey_element['choices']))
elif survey_element['type'] == 'multiplechoice':
if survey_element['variable'] in data:
if data[survey_element['variable']] not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']],
survey_element['variable'],
survey_element['choices']))
errors += self._survey_element_validation(survey_element, data)
return errors

View File

@@ -4,6 +4,7 @@ import json
from awx.main.tasks import RunJob
from awx.main.models import (
Job,
JobTemplate,
WorkflowJobTemplate
)
@@ -78,6 +79,18 @@ def test_job_args_unredacted_passwords(job):
assert extra_vars['secret_key'] == 'my_password'
def test_update_kwargs_survey_invalid_default(survey_spec_factory):
spec = survey_spec_factory('var2')
spec['spec'][0]['required'] = False
spec['spec'][0]['min'] = 3
spec['spec'][0]['default'] = 1
jt = JobTemplate(name="test-jt", survey_spec=spec, survey_enabled=True, extra_vars="var2: 2")
defaulted_extra_vars = jt._update_unified_job_kwargs()
assert 'extra_vars' in defaulted_extra_vars
# Make sure we did not set the invalid default of 1
assert json.loads(defaulted_extra_vars['extra_vars'])['var2'] == 2
class TestWorkflowSurveys:
def test_update_kwargs_survey_defaults(self, survey_spec_factory):
"Assure that the survey default over-rides a JT variable"

View File

@@ -147,7 +147,17 @@ def test_https_logging_handler_splunk_auth_info():
('http://localhost', None, 'http://localhost'),
('http://localhost', 80, 'http://localhost'),
('http://localhost', 8080, 'http://localhost:8080'),
('https://localhost', 443, 'https://localhost:443')
('https://localhost', 443, 'https://localhost:443'),
('ftp://localhost', 443, 'ftp://localhost:443'),
('https://localhost:550', 443, 'https://localhost:550'),
('https://localhost:yoho/foobar', 443, 'https://localhost:443/foobar'),
('https://localhost:yoho/foobar', None, 'https://localhost:yoho/foobar'),
('http://splunk.server:8088/services/collector/event', 80,
'http://splunk.server:8088/services/collector/event'),
('http://splunk.server/services/collector/event', 80,
'http://splunk.server/services/collector/event'),
('http://splunk.server/services/collector/event', 8088,
'http://splunk.server:8088/services/collector/event'),
])
def test_https_logging_handler_http_host_format(host, port, normalized):
handler = HTTPSHandler(host=host, port=port)

View File

@@ -6,6 +6,7 @@ import logging
import json
import requests
import time
import urlparse
from concurrent.futures import ThreadPoolExecutor
from copy import copy
from requests.exceptions import RequestException
@@ -148,10 +149,21 @@ class BaseHTTPSHandler(logging.Handler):
def get_http_host(self):
host = self.host or ''
if not host.startswith('http'):
host = 'http://%s' % self.host
if self.port != 80 and self.port is not None:
host = '%s:%s' % (host, str(self.port))
# urlparse requires scheme to be provided, default to use http if
# missing
if not urlparse.urlsplit(host).scheme:
host = 'http://%s' % host
parsed = urlparse.urlsplit(host)
# Insert self.port if its special and port number is either not
# given in host or given as non-numerical
try:
port = parsed.port or self.port
except ValueError:
port = self.port
if port not in (80, None):
new_netloc = '%s:%s' % (parsed.hostname, port)
return urlparse.urlunsplit((parsed.scheme, new_netloc, parsed.path,
parsed.query, parsed.fragment))
return host
def get_post_kwargs(self, payload_input):

View File

@@ -108,8 +108,8 @@
- name: update project using insights
uri:
url: "{{insights_url}}/r/insights/v1/maintenance?ansible=true"
user: "{{scm_username|quote}}"
password: "{{scm_password|quote}}"
user: "{{scm_username}}"
password: "{{scm_password}}"
force_basic_auth: yes
when: scm_type == 'insights'
register: insights_output
@@ -124,8 +124,8 @@
get_url:
url: "{{insights_url}}/r/insights/v3/maintenance/{{item.maintenance_id}}/playbook"
dest: "{{project_path|quote}}/{{item.name}}-{{item.maintenance_id}}.yml"
url_username: "{{scm_username|quote}}"
url_password: "{{scm_password|quote}}"
url_username: "{{scm_username}}"
url_password: "{{scm_password}}"
force_basic_auth: yes
force: yes
when: scm_type == 'insights' and item.name != None
@@ -136,8 +136,8 @@
get_url:
url: "{{insights_url}}/r/insights/v3/maintenance/{{item.maintenance_id}}/playbook"
dest: "{{project_path|quote}}/insights-plan-{{item.maintenance_id}}.yml"
url_username: "{{scm_username|quote}}"
url_password: "{{scm_password|quote}}"
url_username: "{{scm_username}}"
url_password: "{{scm_password}}"
force_basic_auth: yes
force: yes
when: scm_type == 'insights' and item.name == None

View File

@@ -9,7 +9,7 @@
</a>
<span class="HostEvent-title">{{event.host_name}}</span>
<!-- close -->
<button ui-sref="jobResult" type="button" class="close">
<button ng-click="closeHostEvent()" type="button" class="close">
<i class="fa fa-times-circle"></i>
</button>
</div>
@@ -64,7 +64,7 @@
<!-- controls -->
<div class="HostEvent-controls">
<button ui-sref="jobResult" class="btn btn-sm btn-default HostEvent-close">Close</button>
<button ng-click="closeHostEvent()" class="btn btn-sm btn-default HostEvent-close">Close</button>
</div>
</div>
</div>

View File

@@ -19,7 +19,8 @@
var container = document.getElementById(el);
var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line
lineNumbers: true,
mode: mode
mode: mode,
readOnly: true
});
editor.setSize("100%", 200);
editor.getDoc().setValue(data);
@@ -29,6 +30,19 @@
return $state.current.name === name;
};
$scope.getActiveHostIndex = function(){
var result = $scope.hostResults.filter(function( obj ) {
return obj.id === $scope.event.id;
});
return $scope.hostResults.indexOf(result[0]);
};
$scope.closeHostEvent = function() {
// Unbind the listener so it doesn't fire when we close the modal via navigation
$('#HostEvent').off('hidden.bs.modal');
$state.go('jobDetail');
};
var init = function(){
hostEvent.event_name = hostEvent.event;
$scope.event = _.cloneDeep(hostEvent);
@@ -81,6 +95,10 @@
}
}
$('#HostEvent').modal('show');
$('#HostEvent').on('hidden.bs.modal', function () {
$scope.closeHostEvent();
});
};
init();
}];

View File

@@ -38,7 +38,8 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
// used for tag search
$scope.list = {
basePath: jobData.related.job_events
basePath: jobData.related.job_events,
name: 'job_events'
};
// used for tag search
@@ -450,13 +451,6 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
var getSkeleton = function(url) {
jobResultsService.getEvents(url)
.then(events => {
// old job check: if the job is complete, there is result stdout, and
// there are no job events, it's an old job
if ($scope.jobFinished) {
$scope.showLegacyJobErrorMessage = $scope.job.result_stdout.length &&
!events.results.length;
}
events.results.forEach(event => {
if (event.start_line === 0 && event.end_line === 0) {
$scope.isOld++;

View File

@@ -1368,4 +1368,33 @@ function(ConfigurationUtils, i18n, $rootScope) {
});
}
};
}])
.directive('awRequireMultiple', [function() {
return {
require: 'ngModel',
link: function postLink(scope, element, attrs, ngModel) {
// Watch for changes to the required attribute
attrs.$observe('required', function(value) {
if(value) {
ngModel.$validators.required = function (value) {
if(angular.isArray(value)) {
if(value.length === 0) {
return false;
}
else {
return (!value[0] || value[0] === "") ? false : true;
}
}
else {
return false;
}
};
}
else {
delete ngModel.$validators.required;
}
});
}
};
}]);

View File

@@ -1,5 +1,5 @@
<div>
<select class="form-control SurveyMaker-previewSelect" ng-model="selectedValue" multi-select ng-required="isRequired" ng-disabled="isDisabled">
<select class="form-control SurveyMaker-previewSelect" ng-model="selectedValue" multi-select ng-required="isRequired" ng-disabled="isDisabled" aw-require-multiple>
<option ng-repeat="choice in choices" value="{{choice}}">{{choice}}</option>
</select>
</div>

View File

@@ -207,7 +207,7 @@ export default ['$state','moment', '$timeout', '$window',
}
function update() {
let userCanAddEdit = (scope.workflowJobTemplateObjt && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate;
let userCanAddEdit = (scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate;
if(scope.dimensionsSet) {
// Declare the nodes
let nodes = tree.nodes(scope.treeData),
@@ -813,7 +813,7 @@ export default ['$state','moment', '$timeout', '$window',
function add_node() {
this.on("click", function(d) {
if((scope.workflowJobTemplateObjt && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) {
if((scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) {
scope.addNode({
parent: d,
betweenTwoNodes: false
@@ -824,7 +824,7 @@ export default ['$state','moment', '$timeout', '$window',
function add_node_between() {
this.on("click", function(d) {
if((scope.workflowJobTemplateObjt && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) {
if((scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) {
scope.addNode({
parent: d,
betweenTwoNodes: true
@@ -835,7 +835,7 @@ export default ['$state','moment', '$timeout', '$window',
function remove_node() {
this.on("click", function(d) {
if((scope.workflowJobTemplateObjt && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) {
if((scope.workflowJobTemplateObj && scope.workflowJobTemplateObj.summary_fields && scope.workflowJobTemplateObj.summary_fields.user_capabilities && scope.workflowJobTemplateObj.summary_fields.user_capabilities.edit) || scope.canAddWorkflowJobTemplate) {
scope.deleteNode({
nodeToDelete: d
});

View File

@@ -1,8 +1,3 @@
-e git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv
-e git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield
-e git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic
-e git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding
-e git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax
apache-libcloud==1.3.0
appdirs==1.4.2
asgi-amqp==0.4.1

View File

@@ -4,11 +4,6 @@
#
# pip-compile --output-file requirements/requirements.txt requirements/requirements.in
#
git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv
git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic
git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding
git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield
git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax
adal==0.4.3 # via msrestazure
amqp==1.4.9 # via kombu
anyjson==0.3.3 # via kombu

View File

@@ -1,4 +1,3 @@
-e git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax
apache-libcloud==1.3.0
azure==2.0.0rc6
backports.ssl-match-hostname==3.5.0.1

View File

@@ -4,7 +4,6 @@
#
# pip-compile --output-file requirements/requirements_ansible.txt requirements/requirements_ansible.in
#
git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax
adal==0.4.3 # via msrestazure
amqp==1.4.9 # via kombu
anyjson==0.3.3 # via kombu

View File

@@ -0,0 +1 @@
git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax

View File

@@ -0,0 +1,5 @@
git+https://github.com/ansible/ansiconv.git@tower_1.0.0#egg=ansiconv
git+https://github.com/ansible/django-qsstats-magic.git@tower_0.7.2#egg=django-qsstats-magic
git+https://github.com/ansible/dm.xmlsec.binding.git@master#egg=dm.xmlsec.binding
git+https://github.com/ansible/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield
git+https://github.com/chrismeyersfsu/pyrax@tower#egg=pyrax

View File

@@ -126,7 +126,8 @@ setup(
("%s" % docdir, ["docs/licenses/*",]),
("%s" % bindir, ["tools/scripts/ansible-tower-service",
"tools/scripts/failure-event-handler",
"tools/scripts/tower-python"]),
"tools/scripts/tower-python",
"tools/scripts/ansible-tower-setup"]),
("%s" % sosconfig, ["tools/sosreport/tower.py"])]),
options = {
'egg_info': {

View File

@@ -67,3 +67,7 @@ services:
image: postgres:9.4.1
memcached:
image: memcached:alpine
logstash:
build:
context: ./docker-compose
dockerfile: Dockerfile-logstash

View File

@@ -3,7 +3,9 @@ FROM centos:7
ADD Makefile /tmp/Makefile
RUN mkdir /tmp/requirements
ADD requirements/requirements.txt \
requirements/requirements_git.txt \
requirements/requirements_ansible.txt \
requirements/requirements_ansible_git.txt \
requirements/requirements_dev.txt \
requirements/requirements_ansible_uninstall.txt \
requirements/requirements_tower_uninstall.txt \

View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec /var/lib/awx/setup/setup.sh "$@"