Skip to content

Commit 7257f98

Browse files
committed
new lxd provisioner
1 parent 341e7e1 commit 7257f98

File tree

4 files changed

+403
-1
lines changed

4 files changed

+403
-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: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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: '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+
}
75+
end
76+
77+
include_context('with tmpdir')
78+
79+
before(:each) do
80+
FileUtils.mkdir_p(inventory_dir)
81+
end
82+
83+
describe '.run' do
84+
let(:task_input) { {} }
85+
let(:imposter) { instance_double('LXDProvision') }
86+
87+
task_tests = [
88+
[ { action: 'provision', platform: 'test' }, 'success', true ],
89+
[ { action: 'provision', node_name: 'test' }, 'do not specify node_name', false ],
90+
[ { action: 'provision' }, 'platform required', false ],
91+
[ { action: 'tear_down', node_name: 'test' }, 'success', true ],
92+
[ { action: 'tear_down' }, 'node_name required', false ],
93+
[ { action: 'tear_down', platform: 'test' }, 'do not specify platform', false ],
94+
]
95+
96+
task_tests.each do |v|
97+
it "expect arguments '#{v[0]}' return '#{v[1]}'#{v[2] ? '' : ' and raise error'}" do
98+
allow(LXDProvision).to receive(:new).and_return(imposter)
99+
allow(imposter).to receive(:task).and_return(v[1])
100+
allow($stdin).to receive(:read).and_return(v[0].to_json)
101+
if v[2]
102+
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout
103+
else
104+
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout.and raise_error(SystemExit)
105+
end
106+
end
107+
end
108+
end
109+
110+
describe '.task' do
111+
context 'action=provision' do
112+
let(:lxd_platform) { provision_input[:platform] }
113+
114+
before(:each) do
115+
expect(lxd).to receive(:run_local_command)
116+
.with('lxc -q remote get-default').and_return(lxd_remote)
117+
expect(lxd).to receive(:run_local_command)
118+
.with("lxc -q init #{lxd_platform} #{lxd_remote}: #{lxd_flags.join(' ')}").and_return(lxd_init_output)
119+
expect(lxd).to receive(:run_local_command)
120+
.with("lxc -q start #{lxd_remote}:#{container_id}").and_return(lxd_init_output)
121+
end
122+
123+
it 'provisions successfully' do
124+
expect(lxd).to receive(:run_local_command)
125+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime")
126+
127+
LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
128+
129+
expect(File).to receive(:write).with(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
130+
expect(lxd.task(**provision_input)).to eq(provision_output)
131+
end
132+
133+
it 'when retries=0 try once but ignore the raised error' do
134+
provision_input[:retries] = 0
135+
136+
expect(lxd).to receive(:run_local_command)
137+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
138+
139+
expect(lxd.task(**provision_input)).to eq(provision_output)
140+
end
141+
142+
it 'max retries then deletes the instance' do
143+
expect(lxd).to receive(:run_local_command)
144+
.exactly(3).times
145+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
146+
expect(lxd).to receive(:run_local_command)
147+
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")
148+
149+
expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}})
150+
end
151+
end
152+
153+
context 'action=tear_down' do
154+
before(:each) do
155+
File.write(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
156+
end
157+
158+
it 'tears down successfully' do
159+
expect(lxd).to receive(:run_local_command)
160+
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")
161+
162+
LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
163+
File.write(inventory_file, inventory_hash.to_yaml)
164+
165+
expect(lxd.task(**tear_down_input)).to eq(tear_down_output)
166+
end
167+
168+
it 'expect to raise error if no inventory' do
169+
File.delete(inventory_file)
170+
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{Unable to find})
171+
end
172+
173+
it 'expect to raise error if node_name not in inventory' do
174+
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{node_name #{container_id} not found in inventory})
175+
end
176+
end
177+
end
178+
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)