Skip to content

Commit d7fa15b

Browse files
committed
new lxd provisioner
1 parent cfeb84c commit d7fa15b

File tree

4 files changed

+398
-1
lines changed

4 files changed

+398
-1
lines changed

lib/task_helper.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ def get_inventory_hash(inventory_full_path)
99
if File.file?(inventory_full_path)
1010
inventory_hash_from_inventory_file(inventory_full_path)
1111
else
12-
{ 'version' => 2, 'groups' => [{ 'name' => 'docker_nodes', 'targets' => [] }, { 'name' => 'ssh_nodes', 'targets' => [] }, { 'name' => 'winrm_nodes', 'targets' => [] }] }
12+
{
13+
'version' => 2,
14+
'groups' => [
15+
{ 'name' => 'docker_nodes', 'targets' => [] },
16+
{ 'name' => 'lxd_nodes', 'targets' => [] },
17+
{ 'name' => 'ssh_nodes', 'targets' => [] },
18+
{ 'name' => 'winrm_nodes', 'targets' => [] },
19+
]
20+
}
1321
end
1422
end
1523

spec/tasks/lxd_spec.rb

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'webmock/rspec'
5+
require_relative '../../tasks/lxd'
6+
require 'yaml'
7+
8+
RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error
9+
10+
RSpec.shared_context('with tmpdir') do
11+
let(:tmpdir) { @tmpdir } # rubocop:disable RSpec/InstanceVariable
12+
13+
around(:each) do |example|
14+
Dir.mktmpdir('rspec-provision_test') do |t|
15+
@tmpdir = t
16+
example.run
17+
end
18+
end
19+
end
20+
21+
describe 'provision::lxd' do
22+
let(:lxd) { LXDProvision.new }
23+
24+
let(:inventory_dir) { "#{tmpdir}/spec/fixtures" }
25+
let(:inventory_file) { "#{inventory_dir}/litmus_inventory.yaml" }
26+
let(:inventory_hash) { get_inventory_hash(inventory_file) }
27+
28+
let(:provision_input) do
29+
{
30+
action: 'provision',
31+
platform: 'images:foobar/1',
32+
inventory: tmpdir
33+
}
34+
end
35+
let(:tear_down_input) do
36+
{
37+
action: 'tear_down',
38+
node_name: container_id,
39+
inventory: tmpdir
40+
}
41+
end
42+
43+
let(:lxd_remote) { 'fake' }
44+
let(:lxd_flags) { [] }
45+
let(:lxd_platform) { nil }
46+
let(:container_id) { lxd_init_output }
47+
let(:lxd_init_output) { 'random-host' }
48+
49+
let(:provision_output) do
50+
{
51+
status: 'ok',
52+
node_name: container_id,
53+
node: {
54+
uri: container_id,
55+
config: {
56+
transport: 'lxd',
57+
lxd: {
58+
remote: lxd_remote,
59+
'shell-command': 'sh -lc'
60+
}
61+
},
62+
facts: {
63+
provisioner: 'provision::lxd',
64+
container_id: container_id,
65+
platform: lxd_platform
66+
}
67+
}
68+
}
69+
end
70+
71+
let(:tear_down_output) do
72+
{
73+
status: 'ok',
74+
removed: container_id,
75+
}
76+
end
77+
78+
include_context('with tmpdir')
79+
80+
before(:each) do
81+
FileUtils.mkdir_p(inventory_dir)
82+
end
83+
84+
describe '.run' do
85+
let(:task_input) { {} }
86+
let(:imposter) { instance_double('LXDProvision') }
87+
88+
task_tests = [
89+
[ { action: 'provision', platform: 'test' }, 'success', true ],
90+
[ { action: 'provision', node_name: 'test' }, 'do not specify node_name', false ],
91+
[ { action: 'provision' }, 'platform required', false ],
92+
[ { action: 'tear_down', node_name: 'test' }, 'success', true ],
93+
[ { action: 'tear_down' }, 'node_name required', false ],
94+
[ { action: 'tear_down', platform: 'test' }, 'do not specify platform', false ],
95+
]
96+
97+
task_tests.each do |v|
98+
it "expect arguments '#{v[0]}' return '#{v[1]}'#{v[2] ? '' : ' and raise error'}" do
99+
allow(LXDProvision).to receive(:new).and_return(imposter)
100+
allow(imposter).to receive(:task).and_return(v[1])
101+
allow($stdin).to receive(:read).and_return(v[0].to_json)
102+
if v[2]
103+
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout
104+
else
105+
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout.and raise_error(SystemExit)
106+
end
107+
end
108+
end
109+
end
110+
111+
describe '.task' do
112+
context 'action=provision' do
113+
let(:lxd_platform) { provision_input[:platform] }
114+
115+
before(:each) do
116+
expect(lxd).to receive(:run_local_command)
117+
.with('lxc -q remote get-default').and_return(lxd_remote)
118+
expect(lxd).to receive(:run_local_command)
119+
.with("lxc -q init #{lxd_platform} #{lxd_remote}: #{lxd_flags.join(' ')}").and_return(lxd_init_output)
120+
expect(lxd).to receive(:run_local_command)
121+
.with("lxc -q start #{lxd_remote}:#{container_id}").and_return(lxd_init_output)
122+
end
123+
124+
it 'provisions successfully' do
125+
expect(lxd).to receive(:run_local_command)
126+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime")
127+
128+
LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
129+
130+
expect(File).to receive(:write).with(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
131+
expect(lxd.task(**provision_input)).to eq(provision_output)
132+
end
133+
134+
it 'when retries=0 try once but ignore the raised error' do
135+
provision_input[:retries] = 0
136+
137+
expect(lxd).to receive(:run_local_command)
138+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
139+
140+
expect(lxd.task(**provision_input)).to eq(provision_output)
141+
end
142+
143+
it 'max retries then deletes the instance' do
144+
expect(lxd).to receive(:run_local_command)
145+
.exactly(3).times
146+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
147+
expect(lxd).to receive(:run_local_command)
148+
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")
149+
150+
expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}})
151+
end
152+
end
153+
154+
context 'action=tear_down' do
155+
before(:each) do
156+
File.write(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
157+
end
158+
159+
it 'tears down successfully' do
160+
expect(lxd).to receive(:run_local_command)
161+
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")
162+
163+
LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
164+
File.write(inventory_file, inventory_hash.to_yaml)
165+
166+
expect(lxd.task(**tear_down_input)).to eq(tear_down_output)
167+
end
168+
169+
it 'expect to raise error if no inventory' do
170+
File.delete(inventory_file)
171+
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{Unable to find})
172+
end
173+
174+
it 'expect to raise error if node_name not in inventory' do
175+
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{node_name #{container_id} not found in inventory})
176+
end
177+
end
178+
end
179+
end

tasks/lxd.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"puppet_task_version": 1,
3+
"supports_noop": false,
4+
"description": "Provision/Tear down an instance on LXD",
5+
"parameters": {
6+
"action": {
7+
"description": "Action to perform, tear_down or provision",
8+
"type": "Enum[provision, tear_down]",
9+
"default": "provision"
10+
},
11+
"inventory": {
12+
"description": "Location of the inventory file",
13+
"type": "Optional[String[1]]"
14+
},
15+
"node_name": {
16+
"description": "The name of the instance",
17+
"type": "Optional[String[1]]"
18+
},
19+
"platform": {
20+
"description": "LXD image to use, eg images:ubuntu/22.04",
21+
"type": "Optional[String[1]]"
22+
},
23+
"profiles": {
24+
"description": "LXD Profiles to apply",
25+
"type": "Optional[Array[String[1]]]"
26+
},
27+
"storage": {
28+
"description": "LXD Storage pool name",
29+
"type": "Optional[String[1]]"
30+
},
31+
"instance_type": {
32+
"description": "LXD Instance type",
33+
"type": "Optional[String[1]]"
34+
},
35+
"vm": {
36+
"description": "Provision as a virtual-machine instead of a container",
37+
"type": "Optional[Boolean]"
38+
},
39+
"remote": {
40+
"description": "LXD remote, defaults to the LXD client configured default remote",
41+
"type": "Optional[String]"
42+
},
43+
"retries": {
44+
"description": "On provision check the instance is accepting commands, will be deleted if retries exceeded, 0 to disable",
45+
"type": "Integer",
46+
"default": 5
47+
},
48+
"vars": {
49+
"description": "YAML string of key/value pairs to add to the inventory vars section",
50+
"type": "Optional[String[1]]"
51+
}
52+
},
53+
"files": [
54+
"provision/lib/task_helper.rb"
55+
]
56+
}

0 commit comments

Comments
 (0)