Skip to content

Commit f333cd0

Browse files
authored
Merge pull request #251 from h0tw1r3/lxd
LXD provisoner support
2 parents adcbf71 + f43ca83 commit f333cd0

File tree

4 files changed

+450
-1
lines changed

4 files changed

+450
-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: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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_config_show) do
44+
<<-YAML
45+
architecture: x86_64
46+
config:
47+
image.architecture: amd64
48+
image.description: Almalinux 9 amd64 (20240515_23:08)
49+
image.os: Almalinux
50+
image.release: "9"
51+
image.requirements.cdrom_agent: "true"
52+
image.serial: "20240515_23:08"
53+
image.type: disk-kvm.img
54+
image.variant: default
55+
limits.cpu: "2"
56+
limits.memory: 4GB
57+
raw.idmap: |-
58+
uid 1000 1000
59+
gid 1000 1000
60+
volatile.apply_template: create
61+
volatile.base_image: 980e4586bcb618732801ee5ef36bbb7c11beaad4a56862938701354c18b6e706
62+
volatile.cloud-init.instance-id: dbecd5bc-252b-4d4a-a7c5-9fd4c5e39be0
63+
volatile.eth0.hwaddr: 00:16:3e:96:41:fb
64+
volatile.uuid: ccbca107-16bb-450e-9afe-d77e4d100f4b
65+
volatile.uuid.generation: ccbca107-16bb-450e-9afe-d77e4d100f4b
66+
devices:
67+
eth0:
68+
name: eth0
69+
network: incusbr-1000
70+
type: nic
71+
root:
72+
path: /
73+
pool: local
74+
type: disk
75+
ephemeral: false
76+
profiles:
77+
- default
78+
stateful: false
79+
description: ""
80+
YAML
81+
end
82+
83+
let(:lxd_remote) { 'fake' }
84+
let(:lxd_flags) { [] }
85+
let(:lxd_platform) { nil }
86+
let(:container_id) { lxd_init_output }
87+
let(:lxd_init_output) { 'random-host' }
88+
89+
let(:provision_output) do
90+
{
91+
status: 'ok',
92+
node_name: container_id,
93+
node: {
94+
uri: container_id,
95+
config: {
96+
transport: 'lxd',
97+
lxd: {
98+
remote: lxd_remote,
99+
'shell-command': 'sh -lc'
100+
}
101+
},
102+
facts: {
103+
provisioner: 'lxd',
104+
container_id: container_id,
105+
platform: lxd_platform
106+
}
107+
}
108+
}
109+
end
110+
111+
let(:tear_down_output) do
112+
{
113+
status: 'ok',
114+
}
115+
end
116+
117+
include_context('with tmpdir')
118+
119+
before(:each) do
120+
FileUtils.mkdir_p(inventory_dir)
121+
end
122+
123+
describe '.run' do
124+
let(:task_input) { {} }
125+
let(:imposter) { instance_double('LXDProvision') }
126+
127+
task_tests = [
128+
[ { action: 'provision', platform: 'test' }, 'success', true ],
129+
[ { action: 'provision', platform: 'test', vm: true }, 'success', true ],
130+
[ { action: 'provision', node_name: 'test' }, 'do not specify node_name', false ],
131+
[ { action: 'provision' }, 'platform required', false ],
132+
[ { action: 'tear_down', node_name: 'test' }, 'success', true ],
133+
[ { action: 'tear_down' }, 'node_name required', false ],
134+
[ { action: 'tear_down', platform: 'test' }, 'do not specify platform', false ],
135+
]
136+
137+
task_tests.each do |v|
138+
it "expect arguments '#{v[0]}' return '#{v[1]}'#{v[2] ? '' : ' and raise error'}" do
139+
allow(LXDProvision).to receive(:new).and_return(imposter)
140+
allow(imposter).to receive(:task).and_return(v[1])
141+
allow($stdin).to receive(:read).and_return(v[0].to_json)
142+
if v[2]
143+
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout
144+
else
145+
expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout.and raise_error(SystemExit)
146+
end
147+
end
148+
end
149+
end
150+
151+
describe '.task' do
152+
context 'action=provision' do
153+
let(:lxd_platform) { provision_input[:platform] }
154+
155+
before(:each) do
156+
expect(lxd).to receive(:run_local_command)
157+
.with('lxc -q remote get-default').and_return(lxd_remote)
158+
expect(lxd).to receive(:run_local_command)
159+
.with("lxc -q create #{lxd_platform} #{lxd_remote}: #{lxd_flags.join(' ')}").and_return(lxd_init_output)
160+
expect(lxd).to receive(:run_local_command)
161+
.with("lxc -q config show #{lxd_remote}:#{container_id} -e").and_return(lxd_config_show)
162+
if lxd_config_show.match?(%r{image\.requirements\.cdrom_agent:.*true})
163+
expect(lxd).to receive(:run_local_command)
164+
.with("lxc -q config device add #{lxd_remote}:#{container_id} agent disk source=agent:config").and_return(lxd_config_show)
165+
end
166+
expect(lxd).to receive(:run_local_command)
167+
.with("lxc -q start #{lxd_remote}:#{container_id}").and_return(lxd_init_output)
168+
end
169+
170+
it 'provisions successfully' do
171+
expect(lxd).to receive(:run_local_command)
172+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime")
173+
174+
LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
175+
176+
expect(File).to receive(:write).with(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
177+
expect(lxd.task(**provision_input)).to eq(provision_output)
178+
end
179+
180+
it 'when retries=0 try once but ignore the raised error' do
181+
provision_input[:retries] = 0
182+
183+
expect(lxd).to receive(:run_local_command)
184+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
185+
186+
expect(lxd.task(**provision_input)).to eq(provision_output)
187+
end
188+
189+
it 'max retries then deletes the instance' do
190+
expect(lxd).to receive(:run_local_command)
191+
.exactly(3).times
192+
.with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError)
193+
expect(lxd).to receive(:run_local_command)
194+
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")
195+
196+
expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}})
197+
end
198+
end
199+
200+
context 'action=tear_down' do
201+
before(:each) do
202+
File.write(inventory_file, JSON.parse(inventory_hash.to_json).to_yaml)
203+
end
204+
205+
it 'tears down successfully' do
206+
expect(lxd).to receive(:run_local_command)
207+
.with("lxc -q delete #{lxd_remote}:#{container_id} -f")
208+
209+
LXDProvision.new.add_node_to_group(inventory_hash, JSON.parse(provision_output[:node].to_json), 'lxd_nodes')
210+
File.write(inventory_file, inventory_hash.to_yaml)
211+
212+
expect(lxd.task(**tear_down_input)).to eq(tear_down_output)
213+
end
214+
215+
it 'expect to raise error if no inventory' do
216+
File.delete(inventory_file)
217+
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{Unable to find})
218+
end
219+
220+
it 'expect to raise error if node_name not in inventory' do
221+
expect { lxd.task(**tear_down_input) }.to raise_error(StandardError, %r{node_name #{container_id} not found in inventory})
222+
end
223+
end
224+
end
225+
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)