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

(MODULES-7203) Support nonroot task folders #83

Merged
merged 1 commit into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a

## [Unreleased]

### Added

- Ability to specify scheduled tasks in subfolders by prepending the folder path to the task name ([MODULES-7203](https://tickets.puppetlabs.com/browse/.MODULES-7203)).

## [1.0.1] - 2019-03-07

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ All attributes except `name`, `command`, and `trigger` are optional; see the des

The name assigned to the scheduled task.
This will uniquely identify the task on the system.
If specifying a scheduled task inside of subfolder(s), specify the path from root, such as `subfolder/mytaskname`.
This will create the scheduled task `mytaskname` in the container named `subfolder`.
You can only specify a taskname inside of subfolders if the compatibility is set to 2 or higher and when using the taskscheduler2_api provider.

##### `ensure`

Expand Down
6 changes: 6 additions & 0 deletions lib/puppet/provider/scheduled_task/taskscheduler_api2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,10 @@ def validate_trigger(value)

true
end

def validate_name
if @resource[:name].match?(/\\/) && @resource[:compatibility] < 2
raise Puppet::ResourceError, "#{@resource[:name]} specifies a path including subfolders and a compatibility of #{@resource[:compatibility]} - tasks in subfolders are only supported on version 2 and later of the API. Specify a compatibility of 2 or higher or do not specify a subfolder path."
end
end
end
6 changes: 6 additions & 0 deletions lib/puppet/provider/scheduled_task/win32_taskscheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,10 @@ def validate_trigger(value)

true
end

def validate_name
if @resource[:name].match?(/\\/)
raise Puppet::ResourceError, "#{@resource[:name]} specifies a path including subfolders which are not supported by the version of the Task Scheduler API used by this provider. Use the taskscheduler_api2 provider instead."
end
end
end
13 changes: 11 additions & 2 deletions lib/puppet/type/scheduled_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@
end

newparam(:name) do
desc "The name assigned to the scheduled task. This will uniquely
identify the task on the system."
desc "The name assigned to the scheduled task. This will uniquely
identify the task on the system. If specifying a scheduled task
inside of subfolder(s), specify the path from root, such as
`subfolder/mytaskname`. This will create the scheduled task
`mytaskname` in the container named `subfolder`. You can only
specify a taskname inside of subfolders if the compatibility is
set to 2 or higher and when using the taskscheduler2_api provider."

isnamevar
end
Expand Down Expand Up @@ -244,4 +249,8 @@ def is_to_s(current_value=@is)
super(current_value)
end
end

validate do
provider.validate_name if provider.respond_to?(:validate_name)
end
end
31 changes: 26 additions & 5 deletions lib/puppet_x/puppetlabs/scheduled_task/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ def initialize(task_name, compatibility_level = nil)
#
def self.tasks(compatibility = V2_COMPATIBILITY)
enum_task_names(ROOT_FOLDER,
include_child_folders: false,
include_child_folders: true,
include_compatibility: compatibility).map do |item|
task_name_from_task_path(item)
item.partition('\\')[2]
end
end

Expand Down Expand Up @@ -181,9 +181,16 @@ def self.enum_task_names(folder_path = ROOT_FOLDER, enum_options = {})
end

# Returns whether or not the scheduled task exists.
def self.exists?(job_name)
# task name comparison is case insensitive
tasks.any? { |name| name.casecmp(job_name) == 0 }
def self.exists?(task_path)
raise TypeError unless task_path.is_a?(String)
begin
task_folder = task_service.GetFolder(folder_path_from_task_path(task_path))
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa381363(v=vs.85).aspx
_task = task_folder.GetTask(task_name_from_task_path(task_path))
rescue
return false
end
true
end

# Delete the specified task name.
Expand All @@ -201,6 +208,7 @@ def self.delete(task_name)
def save
task_path = @task ? @task.Path : @full_task_path

self.class.create_folder(self.class.folder_path_from_task_path(task_path))
task_folder = self.class.task_service.GetFolder(self.class.folder_path_from_task_path(task_path))
task_user = nil
task_password = nil
Expand Down Expand Up @@ -372,6 +380,19 @@ def self.folder_path_from_task_path(task_path)
path.empty? ? ROOT_FOLDER : path
end

# create_folder returns "S_OK" if created or an HRESULT error code.
# It will create the full path specified, not just a the last child.
def self.create_folder(path)
begin
task_service.GetFolder(path)
rescue WIN32OLERuntimeError => e
unless Error.is_com_error_type(e, Error::ERROR_FILE_NOT_FOUND)
raise Puppet::Error.new( _("GetFolder failed with: %{error}") % { error: e }, e )
end
task_service.GetFolder(ROOT_FOLDER).CreateFolder(path)
end
end

def self.task(task_path)
raise TypeError unless task_path.is_a?(String)
service = task_service
Expand Down
109 changes: 69 additions & 40 deletions spec/integration/puppet_x/puppetlabs/scheduled_task/task_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def create_task(task_name = nil, task_compatiblity = nil, triggers = [])
describe '#enum_task_names' do
before :each do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
end
end

before(:all) do
# Need a V1 task as a test fixture
Expand Down Expand Up @@ -140,61 +140,90 @@ def create_task(task_name = nil, task_compatiblity = nil, triggers = [])
end

describe 'create a task' do
before :each do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
end

before(:all) do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
_, @task_name = create_task(nil, nil, [ manifest_triggers[0] ])
# find the task by name and examine its properties through COM
service = WIN32OLE.new('Schedule.Service')
service.connect()
@task_definition = service
.GetFolder(subject::ROOT_FOLDER)
.GetTask(@task_name)
.Definition
end

after(:all) do
if Puppet.features.microsoft_windows?
subject.delete(@task_name)
context 'in the root folder' do
before :each do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
end
end

context 'given a test task fixture' do
it 'should be enabled by default' do
expect(@task_definition.Settings.Enabled).to eq(true)
before(:all) do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
_, @task_name = create_task(nil, nil, [ manifest_triggers[0] ])
# find the task by name and examine its properties through COM
service = WIN32OLE.new('Schedule.Service')
service.connect()
@task_definition = service
.GetFolder(subject::ROOT_FOLDER)
.GetTask(@task_name)
.Definition
end

it 'should be V2 compatible' do
expect(@task_definition.Settings.Compatibility).to eq(subject::TASK_COMPATIBILITY::TASK_COMPATIBILITY_V2)
after(:all) do
if Puppet.features.microsoft_windows?
subject.delete(@task_name)
end
end

it 'should have a single trigger' do
expect(@task_definition.Triggers.count).to eq(1)
end
context 'given a test task fixture' do
it 'should be enabled by default' do
expect(@task_definition.Settings.Enabled).to eq(true)
end

it 'should be V2 compatible' do
expect(@task_definition.Settings.Compatibility).to eq(subject::TASK_COMPATIBILITY::TASK_COMPATIBILITY_V2)
end

it 'should have a single trigger' do
expect(@task_definition.Triggers.count).to eq(1)
end

it 'should have a trigger of type TimeTrigger' do
expect(@task_definition.Triggers.Item(1).Type).to eq(ST::Trigger::V2::Type::TASK_TRIGGER_TIME)
end

it 'should have a single action' do
expect(@task_definition.Actions.Count).to eq(1)
end

it 'should have a trigger of type TimeTrigger' do
expect(@task_definition.Triggers.Item(1).Type).to eq(ST::Trigger::V2::Type::TASK_TRIGGER_TIME)
it 'should have an action of type Execution' do
expect(@task_definition.Actions.Item(1).Type).to eq(subject::TASK_ACTION_TYPE::TASK_ACTION_EXEC)
end

it 'should have the specified action path' do
expect(@task_definition.Actions.Item(1).Path).to eq('cmd.exe')
end

it 'should have the specified action arguments' do
expect(@task_definition.Actions.Item(1).Arguments).to eq('/c exit 0')
end
end
end

it 'should have a single action' do
expect(@task_definition.Actions.Count).to eq(1)
context 'in a subfolder' do
before :each do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
end

it 'should have an action of type Execution' do
expect(@task_definition.Actions.Item(1).Type).to eq(subject::TASK_ACTION_TYPE::TASK_ACTION_EXEC)
before(:all) do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
task_path = SecureRandom.uuid.to_s + '\puppet_task_' + SecureRandom.uuid.to_s
_, @task_name = create_task(task_path, nil, [ manifest_triggers[0] ])
end

it 'should have the specified action path' do
expect(@task_definition.Actions.Item(1).Path).to eq('cmd.exe')
after(:all) do
if Puppet.features.microsoft_windows?
subject.delete(@task_name)
end
end

it 'should have the specified action arguments' do
expect(@task_definition.Actions.Item(1).Arguments).to eq('/c exit 0')
context 'given a test task fixture' do
it 'should create a folder and place the ' do
ps_cmd = "(Get-ScheduledTask -TaskPath \\#{@task_name.partition('\\')[0]}\\).TaskName"
expect(@task_name.partition('\\')[2]).to be_same_as_powershell_command(ps_cmd)
end
end
end

end

describe 'modify a task' do
Expand All @@ -204,7 +233,7 @@ def create_task(task_name = nil, task_compatiblity = nil, triggers = [])
end

after(:each) do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
subject.delete(@task_name)
end

Expand Down Expand Up @@ -259,7 +288,7 @@ def create_task(task_name = nil, task_compatiblity = nil, triggers = [])
context "should be able to create trigger" do
before :each do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
end
end

before(:all) do
skip('Not on Windows platform') unless Puppet.features.microsoft_windows?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,40 @@
end
end

describe '#validate_name' do

context 'when the compatibility is 1' do
let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'subfolder\Test Task', :command => 'C:\Windows\System32\notepad.exe') }

it 'should raise an error if the compatibility is less than 2 or the provider is win32_taskscheduler' do
case task_provider
when :win32_taskscheduler
expect{resource.validate}.to raise_error(
Puppet::ResourceError,
/Use the taskscheduler_api2 provider instead./
)
when :taskscheduler_api2
expect{resource.validate}.to raise_error(
Puppet::ResourceError,
/Specify a compatibility of 2 or higher or do not specify a subfolder path./
)
end
end
end

context 'when compatibility is 2' do
before :each do
skip('Only check against taskscheduler_api2') unless task_provider == :taskscheduler_api2
end

let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'subfolder\Test Task', :compatibility => 2, :command => 'C:\Windows\System32\notepad.exe') }

it 'should not raise an error if the compatibility is >= 2 and the provider is taskscheduler_api2' do
expect{resource.validate}.not_to raise_error
end
end
end

describe '#flush' do
let(:resource) do
Puppet::Type.type(:scheduled_task).new(
Expand Down