Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time Zone support #2504

Merged
merged 11 commits into from Jun 20, 2017
2 changes: 1 addition & 1 deletion rundeckapp/grails-app/assets/javascripts/momentutil.js
Expand Up @@ -36,7 +36,7 @@
return MomentUtil.formatTime(text, 'h:mm:ss a');
},
formatTimeAtDate : function (text) {
var time = moment(text);
var time = moment.utc(text);
if (!text || !time.isValid()) {
return '';
}
Expand Down
Expand Up @@ -1930,6 +1930,8 @@ class ScheduledExecutionController extends ControllerBase{

def orchestratorPlugins = orchestratorPluginService.listDescriptions()
def globals=frameworkService.getProjectGlobals(scheduledExecution.project).keySet()

def timeZones = scheduledExecutionService.getTimeZones()
def logFilterPlugins = pluginService.listPlugins(LogFilterPlugin)
def fprojects = frameworkService.projectNames(authContext)
return [scheduledExecution :scheduledExecution, crontab:crontab, params:params,
Expand All @@ -1940,6 +1942,7 @@ class ScheduledExecutionController extends ControllerBase{
authorized :scheduledExecutionService.userAuthorizedForJob(request,scheduledExecution,authContext),
nodeStepDescriptions: nodeStepTypes,
stepDescriptions : stepTypes,
timeZones : timeZones,
logFilterPlugins : logFilterPlugins,
projectNames : fprojects,
globalVars : globals
Expand Down Expand Up @@ -2227,6 +2230,8 @@ class ScheduledExecutionController extends ControllerBase{
log.debug("ScheduledExecutionController: create : now returning model data to view...")
def strategyPlugins = scheduledExecutionService.getWorkflowStrategyPluginDescriptions()
def globals=frameworkService.getProjectGlobals(scheduledExecution.project).keySet()

def timeZones = scheduledExecutionService.getTimeZones()
def logFilterPlugins = pluginService.listPlugins(LogFilterPlugin)
def fprojects = frameworkService.projectNames(authContext)
return ['scheduledExecution':scheduledExecution, params:params, crontab:[:],
Expand All @@ -2236,7 +2241,8 @@ class ScheduledExecutionController extends ControllerBase{
orchestratorPlugins : orchestratorPluginService.listDescriptions(),
logFilterPlugins : logFilterPlugins,
projectNames : fprojects,
globalVars :globals]
globalVars :globals,
timeZones :timeZones]
}

private clearEditSession(id='_new'){
Expand Down
Expand Up @@ -64,6 +64,8 @@ class ScheduledExecution extends ExecutionContext {
Boolean multipleExecutions = false
Orchestrator orchestrator

String timeZone

Boolean scheduleEnabled = true
Boolean executionEnabled = true

Expand Down Expand Up @@ -140,6 +142,7 @@ class ScheduledExecution extends ExecutionContext {
logOutputThreshold(maxSize: 256, blank:true, nullable: true)
logOutputThresholdAction(maxSize: 256, blank:true, nullable: true,inList: ['halt','truncate'])
logOutputThresholdStatus(maxSize: 256, blank:true, nullable: true)
timeZone(maxSize: 256, blank: true, nullable: true)
}

static mapping = {
Expand Down Expand Up @@ -231,6 +234,9 @@ class ScheduledExecution extends ExecutionContext {
if(orchestrator){
map.orchestrator=orchestrator.toMap();
}
if(timeZone){
map.timeZone=timeZone
}

if(options){
map.options = []
Expand Down Expand Up @@ -324,6 +330,7 @@ class ScheduledExecution extends ExecutionContext {
se.uuid = data.uuid
}
se.timeout = data.timeout?data.timeout.toString():null
se.timeZone = data.timeZone?data.timeZone.toString():null
se.retry = data.retry?data.retry.toString():null
if(data.options){
TreeSet options=new TreeSet()
Expand Down
3 changes: 3 additions & 0 deletions rundeckapp/grails-app/i18n/messages.properties
Expand Up @@ -1451,6 +1451,9 @@ form.option.multivalueAllSelected.label=Select All Values by Default
project.configuration.extra.category.gui.description=Additional configuration for the user interface for this project
project.configuration.extra.category.gui.title=User Interface
api.error.project.archive.failure=Project export request {0} failed: {1}
scheduledExecution.property.timezone.prompt=Time Zone
scheduledExecution.property.timezone.description=A valid Time Zone, either an abbreviation such as "PST", a full name such as "America/Los_Angeles",or a custom ID such as "GMT-8:00".
scheduledExecution.timezone.error.message=Non Valid Time Zone: {0}
execution.show.log.html.button.title=HTML
execution.show.log.text.button.title=Text
execution.show.log.download.button.title=Download
Expand Down
3 changes: 3 additions & 0 deletions rundeckapp/grails-app/i18n/messages_es_419.properties
Expand Up @@ -1446,6 +1446,9 @@ form.option.multivalueAllSelected.label=Select All Values by Default
project.configuration.extra.category.gui.description=Additional configuration for the Jobs for this project
project.configuration.extra.category.gui.title=User Interface
api.error.project.archive.failure=Project export request {0} failed: {1}
scheduledExecution.property.timezone.prompt=Zona horaria
scheduledExecution.property.timezone.description=Una Zona Horaria válidad, puede ser una abreviatura como "PST", o un nombre copleto como "America/Los_Angeles", o un ID personalizado como "GMT-8:00".
scheduledExecution.timezone.error.message=Zona horaria inválida: {0}
execution.show.log.html.button.title=HTML
execution.show.log.text.button.title=Text
execution.show.log.download.button.title=Download
Expand Down
Expand Up @@ -1283,9 +1283,16 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing
def cronExpression = se.generateCrontabExression()
try {
log.info("creating trigger with crontab expression: " + cronExpression)
trigger = TriggerBuilder.newTrigger().withIdentity(se.generateJobScheduledName(), se.generateJobGroupName())
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build()
if(se.timeZone){
log.info("creating trigger with time zone: " + se.timeZone)
trigger = TriggerBuilder.newTrigger().withIdentity(se.generateJobScheduledName(), se.generateJobGroupName())
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression).inTimeZone(TimeZone.getTimeZone(se.timeZone)))
.build()
}else {
trigger = TriggerBuilder.newTrigger().withIdentity(se.generateJobScheduledName(), se.generateJobGroupName())
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build()
}
} catch (java.text.ParseException ex) {
throw new RuntimeException("Failed creating trigger. Invalid cron expression: " + cronExpression )
}
Expand Down Expand Up @@ -1929,6 +1936,15 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing
"Invalid: {0}")
}
}
if(scheduledExecution.timeZone){
TimeZone tz = TimeZone.getTimeZone(scheduledExecution.timeZone,false)
if(tz == null){
failed = true
scheduledExecution.errors.rejectValue('timeZone',
'scheduledExecution.timezone.error.message', [scheduledExecution.timeZone] as Object[],
"Invalid: {0}")
}
}
} else {
//update schedule owner, in case disabling schedule on a different node
//set nextExecution of non-scheduled job to be far in the future so that query results can sort correctly
Expand Down Expand Up @@ -3007,6 +3023,15 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing
'scheduledExecution.crontabString.noschedule.message', [genCron] as Object[], "invalid: {0}")
}
}
if(scheduledExecution.timeZone){
TimeZone tz = TimeZone.getTimeZone(scheduledExecution.timeZone,false)
if(tz == null){
failed = true
scheduledExecution.errors.rejectValue('timeZone',
'scheduledExecution.timezone.error.message', [scheduledExecution.timeZone] as Object[],
"Invalid: {0}")
}
}
} else {
//set nextExecution of non-scheduled job to be far in the future so that query results can sort correctly
scheduledExecution.nextExecution = new Date(ScheduledExecutionService.TWO_HUNDRED_YEARS)
Expand Down Expand Up @@ -3453,6 +3478,10 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing
ScheduledExecution.findByUuidAndProject(uuid, project)
}


def getTimeZones(){
TimeZone.getAvailableIDs()
}
def isProjectExecutionEnabled(String project){
def fwProject = frameworkService.getFrameworkProject(project)
def disableEx = fwProject.getProjectProperties().get(CONF_PROJECT_DISABLE_EXECUTION)
Expand Down
Expand Up @@ -324,13 +324,13 @@ class StateMapping {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
sdf.setTimeZone(TimeZone.getDefault());
sdf.format(date)
}

static Date decodeDate(String date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
sdf.setTimeZone(TimeZone.getDefault());
sdf.parse(date)
}

Expand Down
26 changes: 26 additions & 0 deletions rundeckapp/grails-app/views/scheduledExecution/_edit.gsp
Expand Up @@ -880,12 +880,38 @@ function getCurSEID(){
<g:javascript>
<wdgt:eventHandlerJS for="scheduledTrue" state="unempty">
<wdgt:action visible="true" targetSelector="#scheduledExecutionEditCrontab"/>
<wdgt:action visible="true" targetSelector="#scheduledExecutionEditTZ"/>
</wdgt:eventHandlerJS>
<wdgt:eventHandlerJS for="scheduledFalse" state="unempty" >
<wdgt:action visible="false" target="scheduledExecutionEditCrontab"/>
<wdgt:action visible="false" targetSelector="#scheduledExecutionEditTZ"/>
</wdgt:eventHandlerJS>
</g:javascript>
</div>

<div class="form-group" style="${wdgt.styleVisible(if:scheduledExecution?.scheduled)}" id="scheduledExecutionEditTZ">
<div class="${labelColSize} control-label text-form-label">
<g:message code="scheduledExecution.property.timezone.prompt" />
</div>
<div class="${fieldColHalfSize}">
<input type='text' name="timeZone" value="${enc(attr:scheduledExecution?.timeZone)}"
id="timeZone" class="form-control"/>

<span class="help-block">
<g:message code="scheduledExecution.property.timezone.description" />
</span>
</div>
<g:javascript>
fireWhenReady('timeZone',function(){
var timeZonesDataArr = loadJsonData('timeZonesData');
jQuery("#timeZone").devbridgeAutocomplete({
lookup: timeZonesDataArr
});
});
</g:javascript>
</div>

</div>
%{-- scheduleEnabled --}%
<g:if test="${auth.jobAllowedTest(job: scheduledExecution, action: AuthConstants.ACTION_TOGGLE_SCHEDULE)}">
<div class="form-group">
Expand Down
Expand Up @@ -23,6 +23,7 @@
at <span class="cronselected" style="display:inline;"><g:enc>${scheduledExecution?.hour}</g:enc>
:
<g:enc>${scheduledExecution?.minute?.size()<2 ? "0"+scheduledExecution?.minute : scheduledExecution?.minute}</g:enc>
<g:enc>${scheduledExecution?.timeZone}</g:enc>
<g:if test="${scheduledExecution?.seconds !='0'}">
: <g:enc>${scheduledExecution?.seconds?.size()<2 ? "0"+scheduledExecution?.seconds : scheduledExecution?.seconds}</g:enc>
</g:if>
Expand Down
1 change: 1 addition & 0 deletions rundeckapp/grails-app/views/scheduledExecution/create.gsp
Expand Up @@ -33,6 +33,7 @@
_onJobEdit(confirm.setNeedsConfirm);
</g:javascript>
<g:embedJSON data="${globalVars ?: []}" id="globalVarData"/>
<g:embedJSON data="${timeZones ?: []}" id="timeZonesData"/>
</head>
<body>

Expand Down
1 change: 1 addition & 0 deletions rundeckapp/grails-app/views/scheduledExecution/edit.gsp
Expand Up @@ -33,6 +33,7 @@
_onJobEdit(confirm.setNeedsConfirm);
</g:javascript>
<g:embedJSON data="${globalVars ?: []}" id="globalVarData"/>
<g:embedJSON data="${timeZones ?: []}" id="timeZonesData"/>
</head>
<body>

Expand Down
Expand Up @@ -2477,6 +2477,100 @@ class ScheduledExecutionServiceSpec extends Specification {
}


def "timezone validations on save"() {
given:
setupDoValidate()
def params = baseJobParams() +[scheduled: true,
crontabString: '0 1 2 3 4 ? *',
useCrontabString: 'true',
timeZone: timezone]
when:

def results = service._dovalidate(params, mockAuth())

then:

results.failed == expectFailed

where:
timezone | expectFailed
null | false
'' | false
'America/Los_Angeles' |false
'GMT-8:00' | false
'PST' | false
'XXXX' |true
'AAmerica/Los_Angeles' | true

}

def "timezone validations on update"(){
given:
setupDoUpdate()
def params = baseJobParams() +[scheduled: true,
crontabString: '0 1 2 3 4 ? *',
useCrontabString: 'true',
timeZone: timezone]
def se = new ScheduledExecution(createJobParams()).save()
service.fileUploadService = Mock(FileUploadService)

when:
def results = service._doupdate([id: se.id.toString()] + params, mockAuth())

then:
results.success == expectSuccess

where:
timezone | expectSuccess
null | true
'' | true
'America/Los_Angeles' |true
'GMT-8:00' | true
'PST' | true
'XXXX' |false
'AAmerica/Los_Angeles' | false
}

@Unroll
def "scheduleJob with or without TimeZone shouldn't fail"() {
given:
service.executionServiceBean = Mock(ExecutionService)
service.quartzScheduler = Mock(Scheduler) {
getListenerManager() >> Mock(ListenerManager)
}
service.frameworkService = Mock(FrameworkService) {
getRundeckBase() >> ''
}
def job = new ScheduledExecution(
createJobParams(
scheduled: true,
scheduleEnabled: true,
executionEnabled: true,
userRoleList: 'a,b',
crontabString: '0 0 10 0 0 ? *',
useCrontabString: 'true',
timeZone: timezone
)
).save()
def scheduleDate = new Date()

when:
def result = service.scheduleJob(job, null, null)

then:
1 * service.executionServiceBean.getExecutionsAreActive() >> executionsAreActive
1 * service.quartzScheduler.scheduleJob(_, _) >> scheduleDate
result == scheduleDate

where:
executionsAreActive | timezone
true | 'America/Los_Angeles'
true | null
true | ''
}



def "project passive mode execution"() {
given:
def projectMock = Mock(IRundeckProject) {
Expand Down Expand Up @@ -2551,4 +2645,5 @@ class ScheduledExecutionServiceSpec extends Specification {


}

}