Merge branch 'release_3.1.0' into jobResultsPerf

This commit is contained in:
jlmitch5 2017-01-12 16:15:09 -05:00 committed by GitHub
commit 7c8c42bfc1
80 changed files with 18728 additions and 267 deletions

View File

@ -251,6 +251,7 @@ class BaseSerializer(serializers.ModelSerializer):
'inventory_update': _('Inventory Sync'),
'system_job': _('Management Job'),
'workflow_job': _('Workflow Job'),
'workflow_job_template': _('Workflow Template'),
}
choices = []
for t in self.get_types():
@ -2708,18 +2709,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
variables_needed_to_start = serializers.ReadOnlyField()
survey_enabled = serializers.SerializerMethodField()
extra_vars = VerbatimField(required=False, write_only=True)
warnings = serializers.SerializerMethodField()
workflow_job_template_data = serializers.SerializerMethodField()
class Meta:
model = WorkflowJobTemplate
fields = ('can_start_without_user_input', 'extra_vars', 'warnings',
fields = ('can_start_without_user_input', 'extra_vars',
'survey_enabled', 'variables_needed_to_start',
'node_templates_missing', 'node_prompts_rejected',
'workflow_job_template_data')
def get_warnings(self, obj):
return obj.get_warnings()
def get_survey_enabled(self, obj):
if obj:
return obj.survey_enabled and 'spec' in obj.survey_spec

View File

@ -1553,12 +1553,14 @@ class InventoryScriptList(ListCreateAPIView):
model = CustomInventoryScript
serializer_class = CustomInventoryScriptSerializer
new_in_210 = True
class InventoryScriptDetail(RetrieveUpdateDestroyAPIView):
model = CustomInventoryScript
serializer_class = CustomInventoryScriptSerializer
new_in_210 = True
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
@ -2904,10 +2906,16 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, GenericAPIView):
def get(self, request, *args, **kwargs):
obj = self.get_object()
data = {}
copy_TF, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
data['can_copy'] = copy_TF
data['warnings'] = messages
can_copy, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
data = OrderedDict([
('can_copy', can_copy), ('can_copy_without_user_input', can_copy),
('templates_unable_to_copy', [] if can_copy else ['all']),
('credentials_unable_to_copy', [] if can_copy else ['all']),
('inventories_unable_to_copy', [] if can_copy else ['all'])
])
if messages and can_copy:
data['can_copy_without_user_input'] = False
data.update(messages)
return Response(data)
def post(self, request, *args, **kwargs):
@ -2938,7 +2946,10 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
always_allow_superuser = False
def update_raw_data(self, data):
obj = self.get_object()
try:
obj = self.get_object()
except PermissionDenied:
return data
extra_vars = data.pop('extra_vars', None) or {}
if obj:
for v in obj.variables_needed_to_start:

View File

@ -328,7 +328,7 @@ class BaseCallbackModule(CallbackBase):
ok=stats.ok,
processed=stats.processed,
skipped=stats.skipped,
artifact_data=stats.custom.get('_run', {})
artifact_data=stats.custom.get('_run', {}) if hasattr(stats, 'custom') else {}
)
with self.capture_event_data('playbook_on_stats', **event_data):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1043,7 +1043,7 @@ class JobTemplateAccess(BaseAccess):
Project.accessible_objects(self.user, 'use_role').exists() or
Inventory.accessible_objects(self.user, 'use_role').exists())
# if reference_obj is provided, determine if it can be coppied
# if reference_obj is provided, determine if it can be copied
reference_obj = data.get('reference_obj', None)
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
@ -1537,22 +1537,28 @@ class WorkflowJobTemplateAccess(BaseAccess):
def can_copy(self, obj):
if self.save_messages:
wfjt_errors = {}
missing_ujt = []
missing_credentials = []
missing_inventories = []
qs = obj.workflow_job_template_nodes
qs.select_related('unified_job_template', 'inventory', 'credential')
for node in qs.all():
node_errors = {}
if node.inventory and self.user not in node.inventory.use_role:
node_errors['inventory'] = 'Prompted inventory %s can not be coppied.' % node.inventory.name
missing_inventories.append(node.inventory.name)
if node.credential and self.user not in node.credential.use_role:
node_errors['credential'] = 'Prompted credential %s can not be coppied.' % node.credential.name
missing_credentials.append(node.credential.name)
ujt = node.unified_job_template
if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
node_errors['unified_job_template'] = (
'Prompted %s %s can not be coppied.' % (ujt._meta.verbose_name_raw, ujt.name))
missing_ujt.append(ujt.name)
if node_errors:
wfjt_errors[node.id] = node_errors
self.messages.update(wfjt_errors)
if missing_ujt:
self.messages['templates_unable_to_copy'] = missing_ujt
if missing_credentials:
self.messages['credentials_unable_to_copy'] = missing_credentials
if missing_inventories:
self.messages['inventories_unable_to_copy'] = missing_inventories
return self.check_related('organization', Organization, {'reference_obj': obj}, mandatory=True)

View File

@ -96,12 +96,12 @@ class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--older_than',
dest='older_than',
default=None,
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
default='30d',
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 30d.'),
make_option('--granularity',
dest='granularity',
default=None,
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
default='1w',
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 1w.'),
make_option('--module',
dest='module',
default=None,

View File

@ -20,7 +20,7 @@ from django.core.urlresolvers import reverse
# AWX
from awx.main.models.base import * # noqa
from awx.main.models.unified_jobs import * # noqa
from awx.main.models.notifications import JobNotificationMixin
from awx.main.models.notifications import JobNotificationMixin, NotificationTemplate
from awx.main.fields import JSONField
logger = logging.getLogger('awx.main.models.ad_hoc_commands')
@ -157,18 +157,20 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
@property
def notification_templates(self):
all_inventory_sources = set()
all_orgs = set()
for h in self.hosts.all():
for invsrc in h.inventory_sources.all():
all_inventory_sources.add(invsrc)
all_orgs.add(h.inventory.organization)
active_templates = dict(error=set(),
success=set(),
any=set())
for invsrc in all_inventory_sources:
notifications_dict = invsrc.notification_templates
for notification_type in active_templates.keys():
for templ in notifications_dict[notification_type]:
active_templates[notification_type].add(templ)
base_notification_templates = NotificationTemplate.objects
for org in all_orgs:
for templ in base_notification_templates.filter(organization_notification_templates_for_errors=org):
active_templates['error'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_success=org):
active_templates['success'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_any=org):
active_templates['any'].add(templ)
active_templates['error'] = list(active_templates['error'])
active_templates['any'] = list(active_templates['any'])
active_templates['success'] = list(active_templates['success'])

View File

@ -134,7 +134,7 @@ class WorkflowNodeBase(CreatedModifiedModel):
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
ignored_dict.update(scan_errors)
for fd in ['inventory', 'credential']:
if getattr(ujt_obj, fd) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
if getattr(ujt_obj, "{}_id".format(fd)) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
missing_dict[fd] = 'Job Template does not have this field and workflow node does not provide it'
data = {}
@ -421,18 +421,22 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
def can_start_without_user_input(self):
'''Return whether WFJT can be launched without survey passwords.'''
return not bool(self.variables_needed_to_start)
return not bool(
self.variables_needed_to_start or
self.node_templates_missing() or
self.node_prompts_rejected())
def get_warnings(self):
warning_data = {}
for node in self.workflow_job_template_nodes.all():
if node.unified_job_template is None:
warning_data[node.pk] = 'Node is missing a linked unified_job_template'
continue
def node_templates_missing(self):
return [node.pk for node in self.workflow_job_template_nodes.filter(
unified_job_template__isnull=True).all()]
def node_prompts_rejected(self):
node_list = []
for node in self.workflow_job_template_nodes.select_related('unified_job_template').all():
node_prompts_warnings = node.get_prompts_warnings()
if node_prompts_warnings:
warning_data[node.pk] = node_prompts_warnings
return warning_data
node_list.append(node.pk)
return node_list
def user_copy(self, user):
new_wfjt = self.copy_unified_jt()

View File

@ -114,6 +114,8 @@ class TaskManager():
dag = WorkflowDAG(workflow_job)
spawn_nodes = dag.bfs_nodes_to_run()
for spawn_node in spawn_nodes:
if spawn_node.unified_job_template is None:
continue
kv = spawn_node.get_job_kwargs()
job = spawn_node.unified_job_template.create_unified_job(**kv)
spawn_node.job = job

View File

@ -67,6 +67,8 @@ class WorkflowDAG(SimpleDAG):
obj = n['node_object']
job = obj.job
if obj.unified_job_template is None:
continue
if not job:
return False
# Job is about to run or is running. Hold our horses and wait for

View File

@ -120,13 +120,11 @@ class TestWorkflowJobAccess:
access = WorkflowJobTemplateAccess(rando, save_messages=True)
assert not access.can_copy(wfjt)
warnings = access.messages
assert 1 in warnings
assert 'inventory' in warnings[1]
assert 'inventories_unable_to_copy' in warnings
def test_workflow_copy_warnings_jt(self, wfjt, rando, job_template):
wfjt.workflow_job_template_nodes.create(unified_job_template=job_template)
access = WorkflowJobTemplateAccess(rando, save_messages=True)
assert not access.can_copy(wfjt)
warnings = access.messages
assert 1 in warnings
assert 'unified_job_template' in warnings[1]
assert 'templates_unable_to_copy' in warnings

View File

@ -5,7 +5,7 @@ import pytest
# AWX
from awx.main.scheduler.dag_simple import SimpleDAG
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.main.models import Job
from awx.main.models import Job, JobTemplate
from awx.main.models.workflow import WorkflowJobNode
@ -72,6 +72,7 @@ def factory_node():
if status:
j = Job(status=status)
wfn.job = j
wfn.unified_job_template = JobTemplate(name='JT{}'.format(id))
return wfn
return fn

View File

@ -73,7 +73,7 @@ DATABASES = {
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
@ -163,6 +163,12 @@ MAX_EVENT_RES_DATA = 700000
# Note: This setting may be overridden by database settings.
EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024
# Disallow sending session cookies over insecure connections
SESSION_COOKIE_SECURE = True
# Disallow sending csrf cookies over insecure connections
CSRF_COOKIE_SECURE = True
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',

View File

@ -24,11 +24,11 @@ ALLOWED_HOSTS = ['*']
mimetypes.add_type("image/svg+xml", ".svg", True)
mimetypes.add_type("image/svg+xml", ".svgz", True)
MONGO_HOST = '127.0.0.1'
MONGO_PORT = 27017
MONGO_USERNAME = None
MONGO_PASSWORD = None
MONGO_DB = 'system_tracking_dev'
# Disallow sending session cookies over insecure connections
SESSION_COOKIE_SECURE = False
# Disallow sending csrf cookies over insecure connections
CSRF_COOKIE_SECURE = False
# Override django.template.loaders.cached.Loader in defaults.py
TEMPLATE_LOADERS = (

View File

@ -114,7 +114,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html

View File

@ -71,7 +71,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html

View File

@ -44,11 +44,10 @@
color: @list-header-txt;
font-size: 14px;
font-weight: bold;
padding-bottom: 25px;
min-height: 45px;
word-break: break-all;
max-width: 90%;
word-wrap: break-word;
margin-bottom: 20px;
}
.Form-secondaryTitle{

View File

@ -15,7 +15,8 @@ export default ['templateUrl', '$state',
usersDataset: '=',
teamsDataset: '=',
resourceData: '=',
withoutTeamPermissions: '@'
withoutTeamPermissions: '@',
title: '@'
},
controller: controller,
templateUrl: templateUrl('access/add-rbac-resource/rbac-resource'),

View File

@ -8,7 +8,7 @@
<div class="List-titleText ng-binding">
{{ object.name || object.username }}
<div class="List-titleLockup"></div>
Add Permissions
{{ title }}
</div>
</div>
<div class="Form-exitHolder">

View File

@ -11,7 +11,8 @@ export default ['templateUrl',
return {
restrict: 'E',
scope: {
resolve: "="
resolve: "=",
title: "@",
},
controller: controller,
templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'),

View File

@ -9,7 +9,7 @@
<div class="List-titleText ng-binding">
{{ owner.name || owner.username }}
<div class="List-titleLockup"></div>
Add Permissions
{{ title }}
</div>
</div>
<div class="Form-exitHolder">

View File

@ -51,6 +51,10 @@
padding-top: 20px;
}
.AddPermissions-list {
margin-bottom: 20px;
}
.AddPermissions-list .List-searchRow {
height: 0px;
}

View File

@ -88,6 +88,14 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
list.fields.first_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
list.fields.last_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
break;
case 'Teams':
list.fields = {
name: list.fields.name,
organization: list.fields.organization,
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.organization.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
default:
list.fields = {
name: list.fields.name,

View File

@ -16,7 +16,8 @@
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY'
},
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET'
},
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {
@ -38,8 +39,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_ORG_KEY'
},
SOCIAL_AUTH_GITHUB_ORG_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_ORG_SECRET'
},
SOCIAL_AUTH_GITHUB_ORG_NAME: {
@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_TEAM_KEY'
},
SOCIAL_AUTH_GITHUB_TEAM_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_TEAM_SECRET'
},
SOCIAL_AUTH_GITHUB_TEAM_ID: {
@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_KEY'
},
SOCIAL_AUTH_GITHUB_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_SECRET'
}
},
@ -24,8 +25,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'
},
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'
},
SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: {
@ -36,8 +37,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
reset: 'AUTH_LDAP_BIND_DN'
},
AUTH_LDAP_BIND_PASSWORD: {
type: 'password'
type: 'sensitive',
hasShowInputButton: true,
},
AUTH_LDAP_USER_SEARCH: {
type: 'textarea',
@ -84,8 +85,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
reset: 'RADIUS_PORT'
},
RADIUS_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'RADIUS_SECRET'
}
},
@ -29,8 +30,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -20,7 +20,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT'
},
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY'
},
SOCIAL_AUTH_SAML_ORG_INFO: {
@ -56,8 +57,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -12,6 +12,19 @@
float: right
}
.Form-resetAll {
border: none;
padding: 0;
background-color: @white;
margin-right: auto;
color: @default-link;
font-size: 12px;
&:hover {
color: @default-link-hov;
}
}
.Form-tab {
min-width: 77px;
}

View File

@ -64,8 +64,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -22,8 +22,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -30,7 +30,8 @@
reset: 'LOG_AGGREGATOR_USERNAME'
},
LOG_AGGREGATOR_PASSWORD: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'LOG_AGGREGATOR_PASSWORD'
},
LOG_AGGREGATOR_LOGGERS: {
@ -48,8 +49,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -26,8 +26,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -32,8 +32,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@ -283,7 +283,7 @@ CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location',
export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
$stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt,
GetBasePath, GetChoices, KindChange, Empty, OwnerChange, FormSave, Wait,
$state, CreateSelect2, Authorization) {
$state, CreateSelect2, Authorization, i18n) {
ClearScope();
@ -336,13 +336,14 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
});
}
// if the credential is assigned to an organization, allow permission delegation
// do NOT use $scope.organization in a view directive to determine if a credential is associated with an org
// @todo why not? ^ and what is this type check for a number doing - should this be a type check for undefined?
$scope.disablePermissionAssignment = typeof($scope.organization) === 'number' ? false : true;
if ($scope.disablePermissionAssignment) {
$scope.permissionsTooltip = 'Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.';
}
$scope.$watch('organization', function(val) {
if (val === undefined) {
$scope.permissionsTooltip = i18n._('Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.');
} else {
$scope.permissionsTooltip = '';
}
});
setAskCheckboxes();
KindChange({
scope: $scope,
@ -613,5 +614,5 @@ CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert',
'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices',
'KindChange', 'Empty', 'OwnerChange',
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization'
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n',
];

View File

@ -420,9 +420,12 @@ export default
related: {
permissions: {
disabled: 'disablePermissionAssignment',
disabled: '(organization === undefined ? true : false)',
// Do not transition the state if organization is undefined
ngClick: `(organization === undefined ? true : false)||$state.go('credentials.edit.permissions')`,
awToolTip: '{{permissionsTooltip}}',
dataTipWatch: 'permissionsTooltip',
awToolTipTabEnabledInEditMode: true,
dataPlacement: 'top',
basePath: 'api/v1/credentials/{{$stateParams.credential_id}}/access_list/',
search: {

View File

@ -103,7 +103,7 @@ angular.module('InventoryFormDefinition', ['ScanJobsListDefinition'])
add: {
label: i18n._('Add'),
ngClick: "$state.go('.add')",
awToolTip: 'Add a permission',
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD',
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'

View File

@ -68,7 +68,7 @@ export default
searchType: 'select',
actions: {
add: {
ngClick: "addPermission",
ngClick: "$state.go('.add')",
label: i18n._('Add'),
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',

View File

@ -121,6 +121,7 @@ export default
organizations: {
awToolTip: i18n._('Please save before assigning to organizations'),
basePath: 'api/v1/users/{{$stateParams.user_id}}/organizations',
emptyListText: i18n._('Please add user to an Organization.'),
search: {
page_size: '10'
},

View File

@ -122,7 +122,7 @@ export default
add: {
ngClick: "$state.go('.add')",
label: i18n._('Add'),
awToolTip: 'Add a permission',
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; '+ i18n._('ADD'),
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'

View File

@ -66,7 +66,19 @@ angular.module('inventory', [
],
ParentObject: ['groupData', function(groupData) {
return groupData;
}]
}],
UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q',
function(Rest, GetBasePath, $stateParams, $q) {
Rest.setUrl(GetBasePath('unified_jobs'));
var val = $q.defer();
Rest.options()
.then(function(data) {
val.resolve(data.data);
}, function(data) {
val.reject(data);
});
return val.promise;
}]
},
views: {
// clear form template when views render in this substate

View File

@ -120,7 +120,7 @@ export default
Rest.post(job_launch_data)
.success(function(data) {
Wait('stop');
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command || data.workflow_job;
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command;
if($rootScope.portalMode===false && Empty(data.system_job) || (base === 'home')){
// use $state.go with reload: true option to re-instantiate sockets in
@ -131,7 +131,8 @@ export default
if(_.has(data, 'job')) {
goToJobDetails('jobDetail');
}
else if(_.has(data, 'workflow_job')) {
else if(data.type && data.type === 'workflow_job') {
job = data.id;
goToJobDetails('workflowResults');
}
else if(_.has(data, 'ad_hoc_command')) {

View File

@ -7,7 +7,8 @@
export default
angular.module('AllJobsDefinition', ['sanitizeFilter', 'capitalizeFilter'])
.value( 'AllJobsList', {
.factory('AllJobsList', ['i18n', function(i18n) {
return {
name: 'jobs',
basePath: 'unified_jobs',
@ -16,6 +17,7 @@ export default
index: false,
hover: true,
well: false,
emptyListText: i18n._('No jobs have yet run.'),
title: false,
fields: {
@ -115,4 +117,5 @@ export default
ngShow: "(job.status !== 'running' && job.status !== 'waiting' && job.status !== 'pending') && job.summary_fields.user_capabilities.delete"
}
}
});
};
}]);

View File

@ -39,18 +39,18 @@ export default
columnClass: "col-lg-4 col-md-4 col-sm-5 col-xs-7 List-staticColumnAdjacent",
modalColumnClass: 'col-md-8'
},
scm_revision: {
label: i18n._('Revision'),
excludeModal: true,
columnClass: 'col-lg-4 col-md-2 col-sm-3 hidden-xs',
class: 'List-staticColumnAdjacent--monospace'
},
scm_type: {
label: i18n._('Type'),
ngBind: 'project.type_label',
excludeModal: true,
columnClass: 'col-lg-2 col-md-2 col-sm-3 hidden-xs'
},
scm_revision: {
label: i18n._('Revision'),
excludeModal: true,
columnClass: 'col-lg-4 col-md-2 col-sm-3 hidden-xs',
class: 'List-staticColumnAdjacent--monospace'
},
last_updated: {
label: i18n._('Last Updated'),
filter: "longDate",
@ -61,6 +61,14 @@ export default
},
actions: {
refresh: {
mode: 'all',
awToolTip: i18n._("Refresh the page"),
ngClick: "refresh()",
ngShow: "socketStatus === 'error'",
actionClass: 'btn List-buttonDefault',
buttonContent: i18n._('REFRESH')
},
add: {
mode: 'all', // One of: edit, select, all
ngClick: 'addProject()',
@ -68,14 +76,6 @@ export default
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ' + i18n._('ADD'),
ngShow: "canAdd"
},
refresh: {
mode: 'all',
awToolTip: i18n._("Refresh the page"),
ngClick: "refresh()",
ngShow: "socketStatus == 'error'",
actionClass: 'btn List-buttonDefault',
buttonContent: i18n._('REFRESH')
}
},

View File

@ -323,6 +323,7 @@ export default ['i18n', function(i18n) {
headers: {
label: i18n._('HTTP Headers'),
type: 'textarea',
name: 'headers',
rows: 5,
'class': 'Form-formGroup--fullWidth',
awRequiredWhen: {

View File

@ -127,6 +127,16 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
});
};
function isDeletedOrganizationBeingEdited(deleted_organization_id, editing_organization_id) {
if (editing_organization_id === undefined) {
return false;
}
if (deleted_organization_id === editing_organization_id) {
return true;
}
return false;
}
$scope.deleteOrganization = function(id, name) {
var action = function() {
@ -137,7 +147,11 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
Rest.destroy()
.success(function() {
Wait('stop');
$state.reload('organizations');
if (isDeletedOrganizationBeingEdited(id, parseInt($stateParams.organization_id)) === true) {
$state.go('^', null, { reload: true });
} else {
$state.reload('organizations');
}
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, {

View File

@ -285,9 +285,7 @@ export default
}
},
data: {
activityStream: true,
activityStreamTarget: 'job',
activityStreamId: 'id'
activityStream: false,
},
ncyBreadcrumb: {
parent: 'jobs',

View File

@ -198,6 +198,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
msg += 'Please contact your system administrator.';
}
Alert(defaultMsg.hdr, msg);
} else if (status === 409) {
Alert('Conflict', data.conflict || "Resource currently in use.");
} else if (status === 410) {
Alert('Deleted Object', 'The requested object was previously deleted and can no longer be accessed.');
} else if ((status === 'Token is expired') || (status === 401 && data.detail && data.detail === 'Token is expired') ||

View File

@ -484,7 +484,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
function applyValidation(viewValue) {
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
query = elm.attr('data-query');
query = query.replace(/\:value/, encodeURI(viewValue));
query = query.replace(/\:value/, encodeURIComponent(viewValue));
Rest.setUrl(`${basePath}${query}`);
// https://github.com/ansible/ansible-tower/issues/3549
// capturing both success/failure conditions in .then() promise
@ -620,12 +620,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
if (attrs.tipWatch) {
// Add dataTipWatch: 'variable_name'
scope.$watch(attrs.tipWatch, function(newVal, oldVal) {
if (newVal !== oldVal) {
// Where did fixTitle come from?:
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
}
scope.$watch(attrs.tipWatch, function(newVal) {
// Where did fixTitle come from?:
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
});
}
}

View File

@ -1479,7 +1479,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += `<div id="${itm}_tab" `+
`class="Form-tab" `;
html += (this.form.related[itm].ngClick) ? `ng-click="` + this.form.related[itm].ngClick + `" ` : `ng-click="$state.go('${this.form.stateTree}.edit.${itm}')" `;
if (collection.awToolTip){
if (collection.awToolTip && collection.awToolTipTabEnabledInEditMode === true) {
html += `aw-tool-tip="${collection.awToolTip}" ` +
`aw-tip-placement="${collection.dataPlacement}" ` +
`data-tip-watch="${collection.dataTipWatch}" `;
@ -1830,7 +1830,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
// smart-search directive
html += `
<div
ng-hide="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)">
ng-hide="${itm}.length === 0 && (searchTags | isEmpty)">
<smart-search
django-model="${itm}"
search-size="${width}"
@ -1855,7 +1855,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += `
<div
class="row"
ng-show="${itm}.length === 0 && !(${collection.iterator}_searchTags | isEmpty)">
ng-show="${itm}.length === 0 && !(searchTags | isEmpty)">
<div class="col-lg-12 List-searchNoResults">
No records matched your search.
</div>
@ -1865,7 +1865,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST");
html += `<div ng-hide="is_superuser">`;
html += `<div class="List-noItems" ng-show="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)"> ${emptyListText} </div>`;
html += `<div class="List-noItems" ng-show="${itm}.length === 0 && (searchTags | isEmpty)"> ${emptyListText} </div>`;
html += '</div>';
html += `

View File

@ -18,7 +18,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q
return;
}
path = GetBasePath($scope.basePath) || $scope.basePath;
queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page });
queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page.toString() });
$state.go('.', {
[$scope.iterator + '_search']: queryset
});

View File

@ -2,11 +2,12 @@ import directive from './smart-search.directive';
import controller from './smart-search.controller';
import service from './queryset.service';
import DjangoSearchModel from './django-search-model.class';
import smartSearchService from './smart-search.service';
export default
angular.module('SmartSearchModule', [])
.directive('smartSearch', directive)
.controller('SmartSearchController', controller)
.service('QuerySet', service)
.service('SmartSearchService', smartSearchService)
.constant('DjangoSearchModel', DjangoSearchModel);

View File

@ -1,5 +1,5 @@
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory',
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory) {
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'SmartSearchService',
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, SmartSearchService) {
return {
// kick off building a model for a specific endpoint
// this is usually a list's basePath
@ -67,29 +67,120 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
return angular.isObject(params) ? `?${queryset}` : '';
function encodeTerm(value, key){
key = key.replace(/__icontains_DEFAULT/g, "__icontains");
key = key.replace(/__search_DEFAULT/g, "__search");
if (Array.isArray(value)){
return _.map(value, (item) => `${key}=${item}`).join('&') + '&';
let concated = '';
angular.forEach(value, function(item){
item = item.replace(/"|'/g, "");
concated += `${key}=${item}&`;
});
return concated;
}
else {
value = value.replace(/"|'/g, "");
return `${key}=${value}&`;
}
}
},
// encodes a ui smart-search param to a django-friendly param
// operand:key:comparator:value => {operand__key__comparator: value}
encodeParam(param){
let split = param.split(':');
return {[split.slice(0,split.length -1).join('__')] : split[split.length-1]};
encodeParam(params){
// Assumption here is that we have a key and a value so the length
// of the paramParts array will be 2. [0] is the key and [1] the value
let paramParts = SmartSearchService.splitTermIntoParts(params.term);
let keySplit = paramParts[0].split('.');
let exclude = false;
let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false;
if(keySplit[0].match(/^-/g)) {
exclude = true;
keySplit[0] = keySplit[0].replace(/^-/, '');
}
let paramString = exclude ? "not__" : "";
let valueString = paramParts[1];
if(keySplit.length === 1) {
if(params.searchTerm && !lessThanGreaterThan) {
paramString += keySplit[0] + '__icontains_DEFAULT';
}
else if(params.relatedSearchTerm) {
paramString += keySplit[0] + '__search_DEFAULT';
}
else {
paramString += keySplit[0];
}
}
else {
paramString += keySplit.join('__');
}
if(lessThanGreaterThan) {
if(paramParts[1].match(/^>=.*$/)) {
paramString += '__gte';
valueString = valueString.replace(/^(>=)/,"");
}
else if(paramParts[1].match(/^<=.*$/)) {
paramString += '__lte';
valueString = valueString.replace(/^(<=)/,"");
}
else if(paramParts[1].match(/^<.*$/)) {
paramString += '__lt';
valueString = valueString.replace(/^(<)/,"");
}
else if(paramParts[1].match(/^>.*$/)) {
paramString += '__gt';
valueString = valueString.replace(/^(>)/,"");
}
}
return {[paramString] : valueString};
},
// decodes a django queryset param into a ui smart-search tag or set of tags
decodeParam(value, key){
let decodeParamString = function(searchString) {
if(key === 'search') {
// Don't include 'search:' in the search tag
return decodeURIComponent(`${searchString}`);
}
else {
key = key.replace(/__icontains_DEFAULT/g, "");
key = key.replace(/__search_DEFAULT/g, "");
let split = key.split('__');
let decodedParam = searchString;
let exclude = false;
if(key.startsWith('not__')) {
exclude = true;
split = split.splice(1, split.length);
}
if(key.endsWith('__gt')) {
decodedParam = '>' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__lt')) {
decodedParam = '<' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__gte')) {
decodedParam = '>=' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__lte')) {
decodedParam = '<=' + decodedParam;
split = split.splice(0, split.length-1);
}
return exclude ? `-${split.join('.')}:${decodedParam}` : `${split.join('.')}:${decodedParam}`;
}
};
if (Array.isArray(value)){
return _.map(value, (item) => {
return `${key.split('__').join(':')}:${item}`;
return decodeParamString(item);
});
}
else {
return `${key.split('__').join(':')}:${value}`;
return decodeParamString(value);
}
},

View File

@ -1,5 +1,5 @@
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet',
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs) {
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService',
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService) {
let path, relations,
// steps through the current tree of $state configurations, grabs default search params
@ -17,6 +17,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
$scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]);
qs.initFieldset(path, $scope.djangoModel, relations).then((data) => {
$scope.models = data.models;
$scope.options = data.options.data;
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
});
}
@ -27,7 +28,20 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null;
});
return _(stripped).map(qs.decodeParam).flatten().value();
let strippedCopy = _.cloneDeep(stripped);
if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){
for (var key in strippedCopy) {
if (strippedCopy.hasOwnProperty(key)) {
let value = strippedCopy[key];
if(_.isArray(value)){
let index = _.indexOf(value, defaults[key]);
value = value.splice(index, 1)[0];
}
}
}
stripped = strippedCopy;
}
return _(strippedCopy).map(qs.decodeParam).flatten().value();
}
// searchable relationships
@ -38,6 +52,16 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
return flat;
}
function setDefaults(term) {
if ($scope.list.defaultSearchParams) {
return $scope.list.defaultSearchParams(term);
} else {
return {
search: encodeURIComponent(term)
};
}
}
$scope.toggleKeyPane = function() {
$scope.showKeyPane = !$scope.showKeyPane;
};
@ -56,10 +80,37 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
// remove tag, merge new queryset, $state.go
$scope.remove = function(index) {
let removed = qs.encodeParam($scope.searchTags.splice(index, 1)[0]);
let tagToRemove = $scope.searchTags.splice(index, 1)[0];
let termParts = SmartSearchService.splitTermIntoParts(tagToRemove);
let removed;
if (termParts.length === 1) {
removed = setDefaults(tagToRemove);
}
else {
let root = termParts[0].split(".")[0].replace(/^-/, '');
let encodeParams = {
term: tagToRemove
};
if(_.has($scope.options.actions.GET, root)) {
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
encodeParams.relatedSearchTerm = true;
}
else {
encodeParams.searchTerm = true;
}
removed = qs.encodeParam(encodeParams);
}
else {
removed = setDefaults(tagToRemove);
}
}
_.each(removed, (value, key) => {
if (Array.isArray(queryset[key])){
_.remove(queryset[key], (item) => item === value);
// If the array is now empty, remove that key
if(queryset[key].length === 0) {
delete queryset[key];
}
}
else {
delete queryset[key];
@ -79,26 +130,46 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
let params = {},
origQueryset = _.clone(queryset);
function setDefaults(term) {
// "name" and "description" are sane defaults for MOST models, but not ALL!
// defaults may be configured in ListDefinition.defaultSearchParams
if ($scope.list.defaultSearchParams) {
return $scope.list.defaultSearchParams(term);
} else {
return {
or__name__icontains: term,
or__description__icontains: term
};
}
}
// Remove leading/trailing whitespace if there is any
terms = terms.trim();
if(terms && terms !== '') {
_.forEach(terms.split(' '), (term) => {
// Split the terms up
let splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
_.forEach(splitTerms, (term) => {
let termParts = SmartSearchService.splitTermIntoParts(term);
function combineSameSearches(a,b){
if (_.isArray(a)) {
return a.concat(b);
}
else {
if(a) {
return [a,b];
}
}
}
// if only a value is provided, search using default keys
if (term.split(':').length === 1) {
params = _.merge(params, setDefaults(term));
if (termParts.length === 1) {
params = _.merge(params, setDefaults(term), combineSameSearches);
} else {
params = _.merge(params, qs.encodeParam(term));
// Figure out if this is a search term
let root = termParts[0].split(".")[0].replace(/^-/, '');
if(_.has($scope.options.actions.GET, root)) {
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches);
}
else {
params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches);
}
}
// Its not a search term or a related search term - treat it as a string
else {
params = _.merge(params, setDefaults(term), combineSameSearches);
}
}
});

View File

@ -0,0 +1,19 @@
export default [function() {
return {
splitSearchIntoTerms(searchString) {
return searchString.match(/(?:[^\s("')]+|"[^"]*"|'[^']*')+/g);
},
splitTermIntoParts(searchTerm) {
let breakOnColon = searchTerm.match(/(?:[^:"]+|"[^"]*")+/g);
if(breakOnColon.length > 2) {
// concat all the strings after the first one together
let stringsToJoin = breakOnColon.slice(1,breakOnColon.length);
return [breakOnColon[0], stringsToJoin.join(':')];
}
else {
return breakOnColon;
}
}
};
}];

View File

@ -267,7 +267,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
},
views: {
[`modal@${formStateDefinition.name}`]: {
template: `<add-rbac-user-team resolve="$resolve"></add-rbac-user-team>`
template: `<add-rbac-user-team resolve="$resolve" title="Add Permissions"></add-rbac-user-team>`
}
},
resolve: {
@ -332,7 +332,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
},
views: {
[`modal@${formStateDefinition.name}`]: {
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" teams-dataset="$resolve.teamsDataset" selected="allSelected" resource-data="$resolve.resourceData"></add-rbac-resource>`
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" teams-dataset="$resolve.teamsDataset" selected="allSelected" resource-data="$resolve.resourceData" title="Add Users / Teams"></add-rbac-resource>`
}
},
resolve: {
@ -501,7 +501,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
},
views: {
[`modal@${formStateDefinition.name}`]: {
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" selected="allSelected" resource-data="$resolve.resourceData" without-team-permissions="true"></add-rbac-resource>`
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" selected="allSelected" resource-data="$resolve.resourceData" without-team-permissions="true" title="Add Users"></add-rbac-resource>`
}
},
resolve: {

View File

@ -287,20 +287,18 @@
if (data.related &&
data.related.callback) {
Alert('Callback URL',
`<div>
<p>Host callbacks are enabled for this template. The callback URL is:</p>
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</string>
</p>
<p>The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</string>
</p>
</div>`,
`Host callbacks are enabled for this template. The callback URL is:
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</strong>
</p>
<p class="break">The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</strong>
</p>`,
'alert-danger', saveCompleted, null, null,
null, true);
}

View File

@ -423,23 +423,20 @@ export default
if (data.related &&
data.related.callback) {
Alert('Callback URL',
`
<div>
<p>Host callbacks are enabled for this template. The callback URL is:</p>
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</string>
</p>
<p>The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</string>
</p>
</div>
`Host callbacks are enabled for this template. The callback URL is:
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</strong>
</p>
<p class="break">The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</strong>
</p>
`,
'alert-info', saveCompleted, null, null,
'alert-danger', saveCompleted, null, null,
null, true);
}
var orgDefer = $q.defer();

View File

@ -131,7 +131,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
handleSuccessfulDelete();
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
ProcessErrors($scope, data, data.status, null, { hdr: 'Error!',
msg: 'Call to delete workflow job template failed. DELETE returned status: ' + status });
});
}
@ -141,8 +141,8 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
handleSuccessfulDelete();
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to delete job template failed. DELETE returned status: ' + status });
ProcessErrors($scope, data, data.status, null, { hdr: 'Error!',
msg: 'Call to delete job template failed. DELETE returned status: ' + data.status });
});
}
else {
@ -220,7 +220,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
.then(function(result) {
if(result.data.can_copy) {
if(!result.data.warnings || _.isEmpty(result.data.warnings)) {
if(result.data.can_copy_without_user_input) {
// Go ahead and copy the workflow - the user has full priveleges on all the resources
TemplateCopyService.copyWorkflow(template.id)
.then(function(result) {
@ -235,18 +235,40 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
let bodyHtml = `
<div class="Prompt-bodyQuery">
You may not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and may result in an incomplete workflow.
You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.
</div>
<div class="Prompt-bodyTarget">`;
// Go and grab all of the warning strings
_.forOwn(result.data.warnings, function(warning) {
if(warning) {
_.forOwn(warning, function(warningString) {
bodyHtml += '<div>' + warningString + '</div>';
});
}
} );
// List the unified job templates user can not access
if (result.data.templates_unable_to_copy.length > 0) {
bodyHtml += '<div>Unified Job Templates that can not be copied<ul>';
_.forOwn(result.data.templates_unable_to_copy, function(ujt) {
if(ujt) {
bodyHtml += '<li>' + ujt + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted inventories user can not access
if (result.data.inventories_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted inventories that can not be copied<ul>';
_.forOwn(result.data.inventories_unable_to_copy, function(inv) {
if(inv) {
bodyHtml += '<li>' + inv + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted credentials user can not access
if (result.data.credentials_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted credentials that can not be copied<ul>';
_.forOwn(result.data.credentials_unable_to_copy, function(cred) {
if(cred) {
bodyHtml += '<li>' + cred + '</li>';
}
});
bodyHtml += '</ul></div>';
}
bodyHtml += '</div>';

View File

@ -174,7 +174,20 @@ export default [ '$state','moment',
nodeEnter.each(function(d) {
let thisNode = d3.select(this);
if(d.isStartNode) {
if(d.isStartNode && scope.mode === 'details') {
// Overwrite the default root height and width and replace it with a small blue square
rootW = 25;
rootH = 25;
thisNode.append("rect")
.attr("width", rootW)
.attr("height", rootH)
.attr("y", 10)
.attr("rx", 5)
.attr("ry", 5)
.attr("fill", "#337ab7")
.attr("class", "WorkflowChart-rootNode");
}
else if(d.isStartNode && scope.mode !== 'details') {
thisNode.append("rect")
.attr("width", rootW)
.attr("height", rootH)
@ -190,7 +203,6 @@ export default [ '$state','moment',
.attr("dy", ".35em")
.attr("class", "WorkflowChart-startText")
.text(function () { return "START"; })
.attr("display", function() { return scope.mode === 'details' ? 'none' : null;})
.call(add_node);
}
else {
@ -200,15 +212,15 @@ export default [ '$state','moment',
.attr("rx", 5)
.attr("ry", 5)
.attr('stroke', function(d) {
if(d.edgeType) {
if(d.edgeType === "failure") {
return "#d9534f";
}
else if(d.edgeType === "success") {
if(d.job && d.job.status) {
if(d.job.status === "successful"){
return "#5cb85c";
}
else if(d.edgeType === "always"){
return "#337ab7";
else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") {
return "#d9534f";
}
else {
return "#D7D7D7";
}
}
else {
@ -593,15 +605,15 @@ export default [ '$state','moment',
t.selectAll(".rect")
.attr('stroke', function(d) {
if(d.edgeType) {
if(d.edgeType === "failure") {
return "#d9534f";
}
else if(d.edgeType === "success") {
if(d.job && d.job.status) {
if(d.job.status === "successful"){
return "#5cb85c";
}
else if(d.edgeType === "always"){
return "#337ab7";
else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") {
return "#d9534f";
}
else {
return "#D7D7D7";
}
}
else {

View File

@ -126,7 +126,7 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr
},
relaunchJob: function(scope) {
InitiatePlaybookRun({ scope: scope, id: scope.workflow.id,
relaunch: true, job_type: 'workflow_job_template' });
relaunch: true, job_type: 'workflow_job' });
}
};
return val;

View File

@ -81,7 +81,7 @@
<div id="alert-modal-msg" class="alert" ng-bind-html="alertBody"></div>
</div>
<div class="modal-footer">
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-primary">{% trans 'OK' %}</a>
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-default">{% trans 'OK' %}</a>
</div>
</div>
<!-- modal-content -->

View File

@ -3,7 +3,8 @@
describe('Service: QuerySet', () => {
let $httpBackend,
QuerySet,
Authorization;
Authorization,
SmartSearchService;
beforeEach(angular.mock.module('Tower', ($provide) =>{
// @todo: improve app source / write testing utilities for interim
@ -17,9 +18,10 @@ describe('Service: QuerySet', () => {
}));
beforeEach(angular.mock.module('RestServices'));
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => {
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_, _SmartSearchService_) => {
$httpBackend = _$httpBackend_;
QuerySet = _QuerySet_;
SmartSearchService = _SmartSearchService_;
// @todo: improve app source
// config.js / local_settings emit $http requests in the app's run block
@ -33,24 +35,27 @@ describe('Service: QuerySet', () => {
.respond(200, '');
}));
describe('fn encodeQuery', () => {
xit('null/undefined params should return an empty string', () => {
expect(QuerySet.encodeQuery(null)).toEqual('');
expect(QuerySet.encodeQuery(undefined)).toEqual('');
describe('fn encodeParam', () => {
it('should encode parameters properly', () =>{
expect(QuerySet.encodeParam({term: "name:foo", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "-name:foo", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "name:'foo bar'", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "'foo bar'"});
expect(QuerySet.encodeParam({term: "-name:'foo bar'", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "'foo bar'"});
expect(QuerySet.encodeParam({term: "organization:foo", relatedSearchTerm: true})).toEqual({"organization__search_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "-organization:foo", relatedSearchTerm: true})).toEqual({"not__organization__search_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "organization.name:foo", relatedSearchTerm: true})).toEqual({"organization__name" : "foo"});
expect(QuerySet.encodeParam({term: "-organization.name:foo", relatedSearchTerm: true})).toEqual({"not__organization__name" : "foo"});
expect(QuerySet.encodeParam({term: "id:11", searchTerm: true})).toEqual({"id__icontains_DEFAULT" : "11"});
expect(QuerySet.encodeParam({term: "-id:11", searchTerm: true})).toEqual({"not__id__icontains_DEFAULT" : "11"});
expect(QuerySet.encodeParam({term: "id:>11", searchTerm: true})).toEqual({"id__gt" : "11"});
expect(QuerySet.encodeParam({term: "-id:>11", searchTerm: true})).toEqual({"not__id__gt" : "11"});
expect(QuerySet.encodeParam({term: "id:>=11", searchTerm: true})).toEqual({"id__gte" : "11"});
expect(QuerySet.encodeParam({term: "-id:>=11", searchTerm: true})).toEqual({"not__id__gte" : "11"});
expect(QuerySet.encodeParam({term: "id:<11", searchTerm: true})).toEqual({"id__lt" : "11"});
expect(QuerySet.encodeParam({term: "-id:<11", searchTerm: true})).toEqual({"not__id__lt" : "11"});
expect(QuerySet.encodeParam({term: "id:<=11", searchTerm: true})).toEqual({"id__lte" : "11"});
expect(QuerySet.encodeParam({term: "-id:<=11", searchTerm: true})).toEqual({"not__id__lte" : "11"});
});
xit('should encode params to a string', () => {
let params = {
or__created_by: 'Jenkins',
or__modified_by: 'Jenkins',
and__not__status: 'success',
},
result = '?or__created_by=Jenkins&or__modified_by=Jenkins&and__not__status=success';
expect(QuerySet.encodeQuery(params)).toEqual(result);
});
});
xdescribe('fn decodeQuery', () => {
});

View File

@ -0,0 +1,43 @@
'use strict';
describe('Service: SmartSearch', () => {
let SmartSearchService;
beforeEach(angular.mock.module('Tower'));
beforeEach(angular.mock.module('SmartSearchModule'));
beforeEach(angular.mock.inject((_SmartSearchService_) => {
SmartSearchService = _SmartSearchService_;
}));
describe('fn splitSearchIntoTerms', () => {
it('should convert the search string to an array tag strings', () =>{
expect(SmartSearchService.splitSearchIntoTerms('foo')).toEqual(["foo"]);
expect(SmartSearchService.splitSearchIntoTerms('foo bar')).toEqual(["foo", "bar"]);
expect(SmartSearchService.splitSearchIntoTerms('name:foo bar')).toEqual(["name:foo", "bar"]);
expect(SmartSearchService.splitSearchIntoTerms('name:foo description:bar')).toEqual(["name:foo", "description:bar"]);
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar"')).toEqual(["name:\"foo bar\""]);
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]);
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]);
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\'')).toEqual(["name:\'foo bar\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" description:\'bar foo\'')).toEqual(["name:\"foo bar\"", "description:\'bar foo\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" foo')).toEqual(["name:\"foo bar\"", "foo"]);
});
});
describe('fn splitTermIntoParts', () => {
it('should convert the search term to a key and value', () =>{
expect(SmartSearchService.splitTermIntoParts('foo')).toEqual(["foo"]);
expect(SmartSearchService.splitTermIntoParts('foo:bar')).toEqual(["foo", "bar"]);
expect(SmartSearchService.splitTermIntoParts('foo:bar:foobar')).toEqual(["foo", "bar:foobar"]);
expect(SmartSearchService.splitTermIntoParts('name:\"foo bar\"')).toEqual(["name", "\"foo bar\""]);
expect(SmartSearchService.splitTermIntoParts('name:\"foo:bar\"')).toEqual(["name", "\"foo:bar\""]);
expect(SmartSearchService.splitTermIntoParts('name:\'foo bar\'')).toEqual(["name", "\'foo bar\'"]);
expect(SmartSearchService.splitTermIntoParts('name:\'foo:bar\'')).toEqual(["name", "\'foo:bar\'"]);
});
});
});

Binary file not shown.

163
docs/licenses/ntlm-auth.txt Normal file
View File

@ -0,0 +1,163 @@
GNU Lesser General Public License
=================================
_Version 3, 29 June 2007_
_Copyright © 2007 Free Software Foundation, Inc. &lt;<http://fsf.org/>&gt;_
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
### 0. Additional Definitions
As used herein, “this License” refers to version 3 of the GNU Lesser
General Public License, and the “GNU GPL” refers to version 3 of the GNU
General Public License.
“The Library” refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An “Application” is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A “Combined Work” is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the “Linked
Version”.
The “Minimal Corresponding Source” for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The “Corresponding Application Code” for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
### 1. Exception to Section 3 of the GNU GPL
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
### 2. Conveying Modified Versions
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
* **a)** under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
* **b)** under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
### 3. Object Code Incorporating Material from Library Header Files
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
* **a)** Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
* **b)** Accompany the object code with a copy of the GNU GPL and this license
document.
### 4. Combined Works
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
* **a)** Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
* **b)** Accompany the Combined Work with a copy of the GNU GPL and this license
document.
* **c)** For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
* **d)** Do one of the following:
- **0)** Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
- **1)** Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that **(a)** uses at run time
a copy of the Library already present on the user's computer
system, and **(b)** will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
* **e)** Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option **4d0**, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option **4d1**, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
### 5. Combined Libraries
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
* **a)** Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
* **b)** Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
### 6. Revised Versions of the GNU Lesser General Public License
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License “or any later version”
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,9 +1,19 @@
MIT License
Copyright (c) 2013 Alexey Diyan
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2012 Kenneth Reitz
Permission to use, copy, modify and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2013 Ben Toews
Permission to use, copy, modify and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -32,7 +32,7 @@ As stated, workflow job templates can be created with populated `extra_vars`. Th
Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing.
Other than success and failure, a workflow spawned job resource can also end with status 'error' and 'canceled'. When a workflow spawned job resource errors, all branches starting from that job will stop executing while the rest keep their own paces. Canceling a workflow spawned job resource follows the same rules.
Other than success and failure, a workflow spawned job resource can also end with status 'error' and 'canceled'. When a workflow spawned job resource errors, all branches starting from that job will stop executing while the rest continue executing. Canceling a workflow spawned job resource follows the same rules. If the unified job template of the node is null (which could be a result of deleting the unified job template or copying a workflow when the user lacks necessary permissions to use the resource), then the branch should stop executing in this case as well.
A workflow job itself can also be canceled. In this case all its spawned job resources will be canceled if cancelable and following paths stop executing.
@ -47,11 +47,11 @@ Workflow job summary:
```
### Workflow Copy and Relaunch
Other than the normal way of creating workflow job templates, it is also possible to copy existing workflow job templates. The resulting new workflow job template will be identical to which it copies from, except for `name` field which will be appended a text to indicate it's a copy.
Other than the normal way of creating workflow job templates, it is also possible to copy existing workflow job templates. The resulting new workflow job template will be mostly identical to the original, except for `name` field which will be appended a text to indicate it's a copy.
Workflow job templates can be copied by POSTing to endpoint `/workflow_job_templates/\d+/copy/`. After copy finished, the resulting new workflow job template will have identical fields including description, extra_vars, labels, 'launch_type' and survey-related fields (survey_passwords, survey_spec and survey_enabled). More importantly, workflow job template node of the original workflow job template, as well as the topology they bear, will be copied. Note there are RBAC restrictions on determining which workflow job template node is copied. In specific, a workflow job template is allowed to be copied if the user has at least read permission on all related resources like credential and job template. On the other hand, schedules and notification templates of the original workflow job template will not be copied nor shared, and the name of the created workflow job template is the original name plus a special-formatted suffix to indicate its copy origin as well as the copy time, such as 'copy_from_name@10:30:00 am'.
Workflow job templates can be copied by POSTing to endpoint `/workflow_job_templates/\d+/copy/`. After copy finished, the resulting new workflow job template will have identical fields including description, extra_vars, and survey-related fields (survey_spec and survey_enabled). More importantly, workflow job template node of the original workflow job template, as well as the topology they bear, will be copied. Note there are RBAC restrictions on copying workflow job template nodes. A workflow job template is allowed to be copied if the user has permission to add an equivalent workflow job template. If the user performing the copy does not have access to a node's related resources (job template, inventory, or credential), those related fields will be null in the copy's version of the node. Schedules and notification templates of the original workflow job template will not be copied nor shared, and the name of the created workflow job template is the original name plus a special-formatted suffix to indicate its copy origin as well as the copy time, such as 'copy_from_name@10:30:00 am'.
Worflow jobs cannot be copied directly, instead a workflow job is implicitly copied when it needs to relaunch. Relaunching an existing workflow job is done by POSTing to endpoint `/workflow_jobs/\d+/relaunch/`. What happens next is the original workflow job is copied to create a new workflow job. The new workflow job then gets a copy of all nodes of the original as well as the topology they bear. Finally the full-fledged new workflow job is triggered to run, thus fulfilling the purpose of relaunch.
Workflow jobs cannot be copied directly, instead a workflow job is implicitly copied when it needs to relaunch. Relaunching an existing workflow job is done by POSTing to endpoint `/workflow_jobs/\d+/relaunch/`. What happens next is the original workflow job is copied to create a new workflow job. The new workflow job then gets a copy of all nodes of the original as well as the topology they bear. Finally the full-fledged new workflow job is triggered to run, thus fulfilling the purpose of relaunch. Survey password-type answers should also be redacted in the relaunched version of the workflow job.
## Test Coverage
### CRUD-related

View File

@ -7,5 +7,6 @@ boto==2.45.0
psphere==0.5.2
psutil==5.0.0
pyvmomi==6.5
pywinrm[kerberos]==0.2.2
secretstorage==2.3.1
shade==1.13.1

View File

@ -64,8 +64,10 @@ msrestazure==0.4.6 # via azure-common
munch==2.0.4 # via shade
netaddr==0.7.18 # via oslo.config, oslo.utils, python-neutronclient
netifaces==0.10.5 # via oslo.utils, shade
ntlm-auth==1.0.2 # via requests-ntlm
oauthlib==2.0.1 # via requests-oauthlib
openstacksdk==0.9.11 # via python-openstackclient
ordereddict==1.1 # via ntlm-auth
os-client-config==1.24.0 # via openstacksdk, osc-lib, python-magnumclient, python-neutronclient, shade
os-diskconfig-python-novaclient-ext==0.1.3 # via rackspace-novaclient
os-networksv2-python-novaclient-ext==0.26 # via rackspace-novaclient
@ -83,6 +85,7 @@ psutil==5.0.0
pyasn1==0.1.9 # via cryptography
pycparser==2.17 # via cffi
PyJWT==1.4.2 # via adal
pykerberos==1.1.13 # via requests-kerberos
pyparsing==2.1.10 # via cliff, cmd2, oslo.utils
python-cinderclient==1.9.0 # via python-openstackclient, shade
python-dateutil==2.6.0 # via adal, azure-storage
@ -100,24 +103,28 @@ python-swiftclient==3.2.0 # via python-heatclient, python-troveclient, shade
python-troveclient==2.7.0 # via shade
pytz==2016.10 # via babel, oslo.serialization, oslo.utils
pyvmomi==6.5
pywinrm[kerberos]==0.2.2
PyYAML==3.12 # via cliff, os-client-config, psphere, python-heatclient, python-ironicclient, python-mistralclient
rackspace-auth-openstack==1.3 # via rackspace-novaclient
rackspace-novaclient==2.1
rax-default-network-flags-python-novaclient-ext==0.4.0 # via rackspace-novaclient
rax-scheduled-images-python-novaclient-ext==0.3.1 # via rackspace-novaclient
requests-kerberos==0.11.0 # via pywinrm
requests-ntlm==1.0.0 # via pywinrm
requests-oauthlib==0.7.0 # via msrest
requests==2.11.1 # via adal, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, pyvmomi, requests-oauthlib
requests==2.11.1 # via adal, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, pyvmomi, pywinrm, requests-kerberos, requests-ntlm, requests-oauthlib
requestsexceptions==1.1.3 # via os-client-config, shade
rfc3986==0.4.1 # via oslo.config
secretstorage==2.3.1
shade==1.13.1
simplejson==3.10.0 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient, python-troveclient
six==1.10.0 # via cliff, cryptography, debtcollector, keystoneauth1, mock, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-swiftclient, python-troveclient, pyvmomi, shade, stevedore, warlock
six==1.10.0 # via cliff, cryptography, debtcollector, keystoneauth1, mock, ntlm-auth, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-swiftclient, python-troveclient, pyvmomi, pywinrm, shade, stevedore, warlock
stevedore==1.19.1 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient, python-magnumclient
suds==0.4 # via psphere
unicodecsv==0.14.1 # via cliff
warlock==1.2.0 # via python-glanceclient
wrapt==1.10.8 # via debtcollector, positional
xmltodict==0.10.2 # via pywinrm
# The following packages are considered to be unsafe in a requirements file:
# setuptools # via cryptography