Skip to content

Commit

Permalink
new lxd provisioner
Browse files Browse the repository at this point in the history
  • Loading branch information
h0tw1r3 committed May 5, 2024
1 parent 9435bc4 commit 30f2957
Show file tree
Hide file tree
Showing 4 changed files with 403 additions and 1 deletion.
10 changes: 9 additions & 1 deletion lib/task_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ def get_inventory_hash(inventory_full_path)
if File.file?(inventory_full_path)
inventory_hash_from_inventory_file(inventory_full_path)
else
{ 'version' => 2, 'groups' => [{ 'name' => 'docker_nodes', 'targets' => [] }, { 'name' => 'ssh_nodes', 'targets' => [] }, { 'name' => 'winrm_nodes', 'targets' => [] }] }
{
'version' => 2,
'groups' => [
{ 'name' => 'docker_nodes', 'targets' => [] },
{ 'name' => 'lxd_nodes', 'targets' => [] },
{ 'name' => 'ssh_nodes', 'targets' => [] },
{ 'name' => 'winrm_nodes', 'targets' => [] },
]
}
end
end

Expand Down
178 changes: 178 additions & 0 deletions spec/tasks/lxd_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# frozen_string_literal: true

require 'spec_helper'
require 'webmock/rspec'
require_relative '../../tasks/lxd'
require 'yaml'

RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error

RSpec.shared_context('with tmpdir') do
let(:tmpdir) { @tmpdir } # rubocop:disable RSpec/InstanceVariable

around(:each) do |example|
Dir.mktmpdir('rspec-provision_test') do |t|
@tmpdir = t
example.run
end
end
end

describe 'provision::lxd' do
let(:lxd) { LXDProvision.new }

let(:inventory_dir) { "#{tmpdir}/spec/fixtures" }
let(:inventory_file) { "#{inventory_dir}/litmus_inventory.yaml" }
let(:inventory_hash) { get_inventory_hash(inventory_file) }

let(:provision_input) do
{
action: 'provision',
platform: 'images:foobar/1',
inventory: tmpdir
}
end
let(:tear_down_input) do
{
action: 'tear_down',
node_name: container_id,
inventory: tmpdir
}
end

let(:lxd_remote) { 'fake' }
let(:lxd_flags) { [] }
let(:lxd_platform) { nil }
let(:container_id) { lxd_init_output }
let(:lxd_init_output) { 'random-host' }

let(:provision_output) do
{
status: 'ok',
node_name: container_id,
node: {
uri: container_id,
config: {
transport: 'lxd',
lxd: {
remote: lxd_remote,
'shell-command': 'sh -lc'
}
},
facts: {
provisioner: 'lxd',
container_id: container_id,
platform: lxd_platform
}
}
}
end

let(:tear_down_output) do
{
status: 'ok',
}
end

include_context('with tmpdir')

before(:each) do
FileUtils.mkdir_p(inventory_dir)
end

describe '.run' do
let(:task_input) { {} }
let(:imposter) { instance_double('LXDProvision') }

task_tests = [
[ { action: 'provision', platform: 'test' }, 'success', true ],
[ { action: 'provision', node_name: 'test' }, 'do not specify node_name', false ],
[ { action: 'provision' }, 'platform required', false ],
[ { action: 'tear_down', node_name: 'test' }, 'success', true ],
[ { action: 'tear_down' }, 'node_name required', false ],
[ { action: 'tear_down', platform: 'test' }, 'do not specify platform', false ],
]

task_tests.each do |v|
it "expect arguments '#{v[0]}' return '#{v[1]}'#{v[2] ? '' : ' and raise error'}" do
allow(LXDProvision).to receive(:new).and_return(imposter)
allow(imposter).to receive(:task).and_return(v[1])
allow($stdin).to receive(:read).and_return(v[0].to_json)
if v[2]
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout
else
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout.and raise_error(SystemExit)
end
end
end
end

describe '.task' do
context 'action=provision' do
let(:lxd_platform) { provision_input[:platform] }

before(:each) do
expect(lxd).to receive(:run_local_command)
.with('lxc -q remote get-default').and_return(lxd_remote)
expect(lxd).to receive(:run_local_command)
.with("lxc -q init #{lxd_platform} #{lxd_remote}: #{lxd_flags.join(' ')}").and_return(lxd_init_output)
expect(lxd).to receive(:run_local_command)
.with("lxc -q start #{lxd_remote}:#{container_id}").and_return(lxd_init_output)
end

it 'provisions successfully' do

Check failure on line 123 in spec/tasks/lxd_spec.rb

View workflow job for this annotation

GitHub Actions / Spec / Spec tests (Puppet: ~> 7.24, Ruby Ver: 2.7)

provision::lxd .task action=provision provisions successfully Failure/Error: expect(lxd.task(**provision_input)).to eq(provision_output) #<LXDProvision:0x00005621f4497cf0 @action="provision", @retries=1, @platform="images:foobar/1", @node_name=nil, @vars=nil, @inventory_full_path="/tmp/rspec-provision_test20240505-1908-3vykgo/spec/fixtures/litmus_inventory.yaml", @inventory={"version"=>2, "groups"=>[{"name"=>"docker_nodes", "targets"=>[]}, {"name"=>"lxd_nodes", "targets"=>[]}, {"name"=>"ssh_nodes", "targets"=>[]}, {"name"=>"winrm_nodes", "targets"=>[]}]}, @options={}, @lxd_default_remote="fake"> received :run_local_command with unexpected arguments expected: ("lxc -q init images:foobar/1 fake: ") got: ("lxc -q create images:foobar/1 fake: ")

Check failure on line 123 in spec/tasks/lxd_spec.rb

View workflow job for this annotation

GitHub Actions / Spec / Spec tests (Puppet: ~> 8.0, Ruby Ver: 3.2)

provision::lxd .task action=provision provisions successfully Failure/Error: expect(lxd.task(**provision_input)).to eq(provision_output) #<LXDProvision:0x00007f1357d9a4b0 @action="provision", @retries=1, @platform="images:foobar/1", @node_name=nil, @vars=nil, @inventory_full_path="/tmp/rspec-provision_test20240505-1860-yjo4i1/spec/fixtures/litmus_inventory.yaml", @inventory={"version"=>2, "groups"=>[{"name"=>"docker_nodes", "targets"=>[]}, {"name"=>"lxd_nodes", "targets"=>[]}, {"name"=>"ssh_nodes", "targets"=>[]}, {"name"=>"winrm_nodes", "targets"=>[]}]}, @options={}, @lxd_default_remote="fake"> received :run_local_command with unexpected arguments expected: ("lxc -q init images:foobar/1 fake: ") got: ("lxc -q create images:foobar/1 fake: ")
expect(lxd).to receive(:run_local_command)
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime")

LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')

expect(File).to receive(:write).with(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
expect(lxd.task(**provision_input)).to eq(provision_output)
end

it 'when retries=0 try once but ignore the raised error' do

Check failure on line 133 in spec/tasks/lxd_spec.rb

View workflow job for this annotation

GitHub Actions / Spec / Spec tests (Puppet: ~> 7.24, Ruby Ver: 2.7)

provision::lxd .task action=provision when retries=0 try once but ignore the raised error Failure/Error: expect(lxd.task(**provision_input)).to eq(provision_output) #<LXDProvision:0x00005621f402c1e8 @action="provision", @retries=0, @platform="images:foobar/1", @node_name=nil, @vars=nil, @inventory_full_path="/tmp/rspec-provision_test20240505-1908-1iy10xe/spec/fixtures/litmus_inventory.yaml", @inventory={"version"=>2, "groups"=>[{"name"=>"docker_nodes", "targets"=>[]}, {"name"=>"lxd_nodes", "targets"=>[]}, {"name"=>"ssh_nodes", "targets"=>[]}, {"name"=>"winrm_nodes", "targets"=>[]}]}, @options={}, @lxd_default_remote="fake"> received :run_local_command with unexpected arguments expected: ("lxc -q init images:foobar/1 fake: ") got: ("lxc -q create images:foobar/1 fake: ")

Check failure on line 133 in spec/tasks/lxd_spec.rb

View workflow job for this annotation

GitHub Actions / Spec / Spec tests (Puppet: ~> 8.0, Ruby Ver: 3.2)

provision::lxd .task action=provision when retries=0 try once but ignore the raised error Failure/Error: expect(lxd.task(**provision_input)).to eq(provision_output) #<LXDProvision:0x00007f1357d8fdd0 @action="provision", @retries=0, @platform="images:foobar/1", @node_name=nil, @vars=nil, @inventory_full_path="/tmp/rspec-provision_test20240505-1860-yhhwn2/spec/fixtures/litmus_inventory.yaml", @inventory={"version"=>2, "groups"=>[{"name"=>"docker_nodes", "targets"=>[]}, {"name"=>"lxd_nodes", "targets"=>[]}, {"name"=>"ssh_nodes", "targets"=>[]}, {"name"=>"winrm_nodes", "targets"=>[]}]}, @options={}, @lxd_default_remote="fake"> received :run_local_command with unexpected arguments expected: ("lxc -q init images:foobar/1 fake: ") got: ("lxc -q create images:foobar/1 fake: ")
provision_input[:retries] = 0

expect(lxd).to receive(:run_local_command)
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)

expect(lxd.task(**provision_input)).to eq(provision_output)
end

it 'max retries then deletes the instance' do

Check failure on line 142 in spec/tasks/lxd_spec.rb

View workflow job for this annotation

GitHub Actions / Spec / Spec tests (Puppet: ~> 7.24, Ruby Ver: 2.7)

provision::lxd .task action=provision max retries then deletes the instance Failure/Error: expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}}) expected StandardError with message matching /Giving up waiting for fake:random-host/, got #<RSpec::Mocks::MockExpectationError: #<LXDProvision:0x00005621f3fc54c0 @action="provision", @retries...pected: ("lxc -q init images:foobar/1 fake: ") got: ("lxc -q create images:foobar/1 fake: ")> with backtrace: # ./tasks/lxd.rb:26:in `provision' # ./tasks/lxd.rb:118:in `call' # ./tasks/lxd.rb:118:in `task' # ./spec/tasks/lxd_spec.rb:149:in `block (5 levels) in <top (required)>' # ./spec/tasks/lxd_spec.rb:149:in `block (4 levels) in <top (required)>' # ./spec/tasks/lxd_spec.rb:16:in `block (3 levels) in <top (required)>' # /opt/hostedtoolcache/Ruby/2.7.8/x64/lib/ruby/2.7.0/tmpdir.rb:89:in `mktmpdir' # ./spec/tasks/lxd_spec.rb:14:in `block (2 levels) in <top (required)>' # ./vendor/bundle/ruby/2.7.0/bin/rspec:23:in `load' # ./vendor/bundle/ruby/2.7.0/bin/rspec:23:in `<top (required)>' # /opt/hostedtoolcache/Ruby/2.7.8/x64/bin/bundle:23:in `load' # /opt/hostedtoolcache/Ruby/2.7.8/x64/bin/bundle:23:in `<main>'

Check failure on line 142 in spec/tasks/lxd_spec.rb

View workflow job for this annotation

GitHub Actions / Spec / Spec tests (Puppet: ~> 8.0, Ruby Ver: 3.2)

provision::lxd .task action=provision max retries then deletes the instance Failure/Error: expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}}) expected StandardError with message matching /Giving up waiting for fake:random-host/, got #<RSpec::Mocks::MockExpectationError:"#<LXDProvision:0x00007f1357dbf468 @action=\"provision\", @retri...: (\"lxc -q init images:foobar/1 fake: \")\n got: (\"lxc -q create images:foobar/1 fake: \")"> with backtrace: # ./tasks/lxd.rb:26:in `provision' # ./tasks/lxd.rb:118:in `call' # ./tasks/lxd.rb:118:in `task' # ./spec/tasks/lxd_spec.rb:149:in `block (5 levels) in <top (required)>' # ./spec/tasks/lxd_spec.rb:149:in `block (4 levels) in <top (required)>' # ./spec/tasks/lxd_spec.rb:16:in `block (3 levels) in <top (required)>' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/tmpdir.rb:94:in `mktmpdir' # ./spec/tasks/lxd_spec.rb:14:in `block (2 levels) in <top (required)>' # ./vendor/bundle/ruby/3.2.0/bin/rspec:25:in `load' # ./vendor/bundle/ruby/3.2.0/bin/rspec:25:in `<top (required)>' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/cli/exec.rb:58:in `load' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/cli/exec.rb:58:in `kernel_load' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/cli/exec.rb:23:in `run' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/cli.rb:492:in `exec' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/cli.rb:34:in `dispatch' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/cli.rb:28:in `start' # /opt/hostedtoolcache/Ruby/3.2.4/x64/lib/ruby/3.2.0/bundler/friendly_errors.rb:117:in `with_friendly_errors' # /opt/hostedtoolcache/Ruby/3.2.4/x64/bin/bundle:25:in `load' # /opt/hostedtoolcache/Ruby/3.2.4/x64/bin/bundle:25:in `<main>'
expect(lxd).to receive(:run_local_command)
.exactly(3).times
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
expect(lxd).to receive(:run_local_command)
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")

expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}})
end
end

context 'action=tear_down' do
before(:each) do
File.write(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
end

it 'tears down successfully' do
expect(lxd).to receive(:run_local_command)
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")

LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
File.write(inventory_file, inventory_hash.to_yaml)

expect(lxd.task(**tear_down_input)).to eq(tear_down_output)
end

it 'expect to raise error if no inventory' do
File.delete(inventory_file)
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{Unable to find})
end

it 'expect to raise error if node_name not in inventory' do
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{node_name #{container_id} not found in inventory})
end
end
end
end
56 changes: 56 additions & 0 deletions tasks/lxd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"puppet_task_version": 1,
"supports_noop": false,
"description": "Provision/Tear down an instance on LXD",
"parameters": {
"action": {
"description": "Action to perform, tear_down or provision",
"type": "Enum[provision, tear_down]",
"default": "provision"
},
"inventory": {
"description": "Location of the inventory file",
"type": "Optional[String[1]]"
},
"node_name": {
"description": "The name of the instance",
"type": "Optional[String[1]]"
},
"platform": {
"description": "LXD image to use, eg images:ubuntu/22.04",
"type": "Optional[String[1]]"
},
"profiles": {
"description": "LXD Profiles to apply",
"type": "Optional[Array[String[1]]]"
},
"storage": {
"description": "LXD Storage pool name",
"type": "Optional[String[1]]"
},
"instance_type": {
"description": "LXD Instance type",
"type": "Optional[String[1]]"
},
"vm": {
"description": "Provision as a virtual-machine instead of a container",
"type": "Optional[Boolean]"
},
"remote": {
"description": "LXD remote, defaults to the LXD client configured default remote",
"type": "Optional[String]"
},
"retries": {
"description": "On provision check the instance is accepting commands, will be deleted if retries exceeded, 0 to disable",
"type": "Integer",
"default": 5
},
"vars": {
"description": "YAML string of key/value pairs to add to the inventory vars section",
"type": "Optional[String[1]]"
}
},
"files": [
"provision/lib/task_helper.rb"
]
}
Loading

0 comments on commit 30f2957

Please sign in to comment.