bosh-stemcell 1.5.0.pre.1113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/.rspec +3 -0
  2. data/README.md +104 -0
  3. data/Vagrantfile +37 -0
  4. data/bosh-stemcell.gemspec +28 -0
  5. data/lib/bosh/stemcell.rb +4 -0
  6. data/lib/bosh/stemcell/archive.rb +59 -0
  7. data/lib/bosh/stemcell/archive_filename.rb +29 -0
  8. data/lib/bosh/stemcell/aws/ami.rb +58 -0
  9. data/lib/bosh/stemcell/aws/light_stemcell.rb +44 -0
  10. data/lib/bosh/stemcell/aws/region.rb +15 -0
  11. data/lib/bosh/stemcell/builder_command.rb +174 -0
  12. data/lib/bosh/stemcell/builder_options.rb +85 -0
  13. data/lib/bosh/stemcell/disk_image.rb +61 -0
  14. data/lib/bosh/stemcell/infrastructure.rb +49 -0
  15. data/lib/bosh/stemcell/operating_system.rb +31 -0
  16. data/lib/bosh/stemcell/stage_collection.rb +154 -0
  17. data/lib/bosh/stemcell/stage_runner.rb +53 -0
  18. data/lib/bosh/stemcell/version.rb +5 -0
  19. data/lib/monkeypatch/serverspec/backend/exec.rb +86 -0
  20. data/spec/assets/fake-stemcell-aws.tgz +0 -0
  21. data/spec/assets/fake-stemcell-vsphere.tgz +0 -0
  22. data/spec/assets/light-fake-stemcell-aws.tgz +0 -0
  23. data/spec/bosh/monkeypatch/serverspec/backend/exec_spec.rb +46 -0
  24. data/spec/bosh/stemcell/archive_filename_spec.rb +42 -0
  25. data/spec/bosh/stemcell/archive_spec.rb +98 -0
  26. data/spec/bosh/stemcell/aws/ami_spec.rb +30 -0
  27. data/spec/bosh/stemcell/aws/light_stemcell_spec.rb +94 -0
  28. data/spec/bosh/stemcell/aws/region_spec.rb +12 -0
  29. data/spec/bosh/stemcell/builder_command_spec.rb +241 -0
  30. data/spec/bosh/stemcell/builder_options_spec.rb +201 -0
  31. data/spec/bosh/stemcell/disk_image_spec.rb +163 -0
  32. data/spec/bosh/stemcell/infrastructure_spec.rb +66 -0
  33. data/spec/bosh/stemcell/operating_system_spec.rb +47 -0
  34. data/spec/bosh/stemcell/stage_collection_spec.rb +213 -0
  35. data/spec/bosh/stemcell/stage_runner_spec.rb +141 -0
  36. data/spec/bosh/stemcell/version_spec.rb +12 -0
  37. data/spec/bosh/stemcell_spec.rb +6 -0
  38. data/spec/spec_helper.rb +6 -0
  39. data/spec/stemcells/aws_spec.rb +9 -0
  40. data/spec/stemcells/centos_spec.rb +131 -0
  41. data/spec/stemcells/openstack_spec.rb +9 -0
  42. data/spec/stemcells/ubuntu_spec.rb +143 -0
  43. data/spec/stemcells/vsphere_spec.rb +9 -0
  44. data/spec/support/rspec_fire.rb +9 -0
  45. data/spec/support/serverspec.rb +4 -0
  46. data/spec/support/spec_assets.rb +11 -0
  47. data/spec/support/stemcell_image.rb +26 -0
  48. data/spec/support/stemcell_shared_examples.rb +43 -0
  49. data/spec/support/stub_env.rb +5 -0
  50. metadata +236 -0
@@ -0,0 +1,201 @@
1
+ require 'spec_helper'
2
+ require 'bosh/stemcell/builder_options'
3
+ require 'bosh/stemcell/infrastructure'
4
+ require 'bosh/stemcell/operating_system'
5
+
6
+ module Bosh::Stemcell
7
+ describe BuilderOptions do
8
+ subject(:stemcell_builder_options) { described_class.new(env, options) }
9
+ let(:env) { {} }
10
+ let(:options) do
11
+ {
12
+ tarball: 'fake/release.tgz',
13
+ stemcell_version: '007',
14
+ infrastructure: infrastructure,
15
+ operating_system: operating_system
16
+ }
17
+ end
18
+
19
+ let(:infrastructure) { Infrastructure.for('aws') }
20
+ let(:operating_system) { OperatingSystem.for('ubuntu') }
21
+ let(:expected_source_root) { File.expand_path('../../../../..', __FILE__) }
22
+ let(:archive_filename) { instance_double('Bosh::Stemcell::ArchiveFilename', to_s: 'FAKE_STEMCELL.tgz') }
23
+
24
+ before do
25
+ ArchiveFilename.stub(:new).
26
+ with('007', infrastructure, operating_system, 'bosh-stemcell', false).and_return(archive_filename)
27
+ end
28
+
29
+ describe '#initialize' do
30
+ context 'when :tarball is not set' do
31
+ before { options.delete(:tarball) }
32
+
33
+ it 'dies' do
34
+ expect { stemcell_builder_options }.to raise_error('key not found: :tarball')
35
+ end
36
+ end
37
+
38
+ context 'when :stemcell_version is not set' do
39
+ before { options.delete(:stemcell_version) }
40
+
41
+ it 'dies' do
42
+ expect { stemcell_builder_options }.to raise_error('key not found: :stemcell_version')
43
+ end
44
+ end
45
+
46
+ context 'when :infrastructure is not set' do
47
+ before { options.delete(:infrastructure) }
48
+
49
+ it 'dies' do
50
+ expect { stemcell_builder_options }.to raise_error('key not found: :infrastructure')
51
+ end
52
+ end
53
+
54
+ context 'when :operating_system is not set' do
55
+ before { options.delete(:operating_system) }
56
+
57
+ it 'dies' do
58
+ expect { stemcell_builder_options }.to raise_error('key not found: :operating_system')
59
+ end
60
+ end
61
+ end
62
+
63
+ describe '#default' do
64
+ let(:default_disk_size) { 2048 }
65
+ let(:rake_args) { {} }
66
+
67
+ it 'sets stemcell_tgz' do
68
+ result = stemcell_builder_options.default
69
+ expect(result['stemcell_tgz']).to eq(archive_filename.to_s)
70
+ end
71
+
72
+ it 'sets stemcell_image_name' do
73
+ result = stemcell_builder_options.default
74
+ expected_image_name = "#{infrastructure.name}-#{infrastructure.hypervisor}-#{operating_system.name}.raw"
75
+ expect(result['stemcell_image_name']).to eq(expected_image_name)
76
+ end
77
+
78
+ it 'sets stemcell_version' do
79
+ result = stemcell_builder_options.default
80
+ expect(result['stemcell_version']).to eq('007')
81
+ end
82
+
83
+ # rubocop:disable MethodLength
84
+ def self.it_sets_correct_environment_variables
85
+ describe 'setting enviroment variables' do
86
+ let(:env) do
87
+ {
88
+ 'UBUNTU_ISO' => 'fake_ubuntu_iso',
89
+ 'UBUNTU_MIRROR' => 'fake_ubuntu_mirror',
90
+ 'RUBY_BIN' => 'fake_ruby_bin',
91
+ }
92
+ end
93
+
94
+ it 'sets default values for options based in hash' do
95
+ expected_release_micro_manifest_path =
96
+ File.join(expected_source_root, "release/micro/#{infrastructure.name}.yml")
97
+
98
+ result = stemcell_builder_options.default
99
+
100
+ expect(result['stemcell_name']).to eq(
101
+ "bosh-#{infrastructure.name}-#{infrastructure.hypervisor}-#{operating_system.name}")
102
+ expect(result['stemcell_operating_system']).to eq(operating_system.name)
103
+ expect(result['stemcell_infrastructure']).to eq(infrastructure.name)
104
+ expect(result['stemcell_hypervisor']).to eq(infrastructure.hypervisor)
105
+ expect(result['bosh_protocol_version']).to eq('1')
106
+ expect(result['UBUNTU_ISO']).to eq('fake_ubuntu_iso')
107
+ expect(result['UBUNTU_MIRROR']).to eq('fake_ubuntu_mirror')
108
+ expect(result['ruby_bin']).to eq('fake_ruby_bin')
109
+ expect(result['bosh_release_src_dir']).to eq(File.join(expected_source_root, '/release/src/bosh'))
110
+ expect(result['bosh_agent_src_dir']).to eq(File.join(expected_source_root, 'bosh_agent'))
111
+ expect(result['image_create_disk_size']).to eq(default_disk_size)
112
+ expect(result['bosh_micro_enabled']).to eq('yes')
113
+ expect(result['bosh_micro_package_compiler_path']).to eq(
114
+ File.join(expected_source_root, 'bosh-release'))
115
+ expect(result['bosh_micro_manifest_yml_path']).to eq(expected_release_micro_manifest_path)
116
+ expect(result['bosh_micro_release_tgz_path']).to eq('fake/release.tgz')
117
+ end
118
+
119
+ context 'when RUBY_BIN is not set' do
120
+ before { env.delete('RUBY_BIN') }
121
+
122
+ before do
123
+ RbConfig::CONFIG.stub(:[]).with('bindir').and_return('/a/path/to/')
124
+ RbConfig::CONFIG.stub(:[]).with('ruby_install_name').and_return('ruby')
125
+ end
126
+
127
+ it 'uses the RbConfig values' do
128
+ result = stemcell_builder_options.default
129
+
130
+ expect(result['ruby_bin']).to eq('/a/path/to/ruby')
131
+ end
132
+ end
133
+
134
+ context 'when disk_size is not passed' do
135
+ it 'defaults to default disk size for infrastructure' do
136
+ result = stemcell_builder_options.default
137
+
138
+ expect(result['image_create_disk_size']).to eq(default_disk_size)
139
+ end
140
+ end
141
+
142
+ context 'when disk_size is passed' do
143
+ before { options.merge!(disk_size: 1234) }
144
+
145
+ it 'allows user to override default disk_size' do
146
+ result = stemcell_builder_options.default
147
+
148
+ expect(result['image_create_disk_size']).to eq(1234)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ # rubocop:enable MethodLength
154
+
155
+ describe 'infrastructure variation' do
156
+ context 'when infrastruture is aws' do
157
+ let(:infrastructure) { Infrastructure.for('aws') }
158
+
159
+ it_sets_correct_environment_variables
160
+
161
+ it 'has no "image_vsphere_ovf_ovftool_path" key' do
162
+ expect(stemcell_builder_options.default).not_to have_key('image_vsphere_ovf_ovftool_path')
163
+ end
164
+ end
165
+
166
+ context 'when infrastruture is vsphere' do
167
+ let(:infrastructure) { Infrastructure.for('vsphere') }
168
+
169
+ it_sets_correct_environment_variables
170
+
171
+ it 'has an "image_vsphere_ovf_ovftool_path" key' do
172
+ result = stemcell_builder_options.default
173
+
174
+ expect(result['image_vsphere_ovf_ovftool_path']).to be_nil
175
+ end
176
+
177
+ context 'if you have OVFTOOL set in the environment' do
178
+ let(:env) { { 'OVFTOOL' => 'fake_ovf_tool_path' } }
179
+
180
+ it 'sets image_vsphere_ovf_ovftool_path' do
181
+ result = stemcell_builder_options.default
182
+
183
+ expect(result['image_vsphere_ovf_ovftool_path']).to eq('fake_ovf_tool_path')
184
+ end
185
+ end
186
+ end
187
+
188
+ context 'when infrastructure is openstack' do
189
+ let(:infrastructure) { Infrastructure.for('openstack') }
190
+ let(:default_disk_size) { 10240 }
191
+
192
+ it_sets_correct_environment_variables
193
+
194
+ it 'has no "image_vsphere_ovf_ovftool_path" key' do
195
+ expect(stemcell_builder_options.default).not_to have_key('image_vsphere_ovf_ovftool_path')
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+ require 'bosh/stemcell/disk_image'
3
+
4
+ module Bosh::Stemcell
5
+ describe DiskImage do
6
+ let(:shell) { instance_double('Bosh::Core::Shell', run: nil) }
7
+
8
+ let(:kpartx_map_output) { 'add map FAKE_LOOP1p1 (252:3): 0 3997984 linear /dev/loop1 63' }
9
+ let(:options) do
10
+ {
11
+ image_file_path: '/path/to/FAKE_IMAGE',
12
+ image_mount_point: '/fake/mnt'
13
+ }
14
+ end
15
+
16
+ subject(:disk_image) { DiskImage.new(options) }
17
+
18
+ before do
19
+ Bosh::Core::Shell.stub(:new).and_return(shell)
20
+ end
21
+
22
+ describe '#initialize' do
23
+ it 'requires an image_file_path' do
24
+ options.delete(:image_file_path)
25
+ expect { DiskImage.new(options) }.to raise_error /key not found: :image_file_path/
26
+ end
27
+
28
+ it 'requires an mount_point' do
29
+ options.delete(:image_mount_point)
30
+
31
+ dir_mock = class_double('Dir').as_stubbed_const
32
+ dir_mock.should_receive(:mktmpdir).and_return('/fake/tmpdir')
33
+
34
+ expect(DiskImage.new(options).image_mount_point).to eq('/fake/tmpdir')
35
+ end
36
+ end
37
+
38
+ describe '#mount' do
39
+ it 'maps the file to a loop device' do
40
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
41
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
42
+ shell.should_receive(:run).with('sudo kpartx -av /dev/loop0',
43
+ output_command: false).and_return(kpartx_map_output)
44
+
45
+ disk_image.mount
46
+ end
47
+
48
+ it 'mounts the loop device' do
49
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
50
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
51
+ shell.stub(:run).with('sudo kpartx -av /dev/loop0', output_command: false).and_return(kpartx_map_output)
52
+
53
+ shell.should_receive(:run).with('sudo mount /dev/mapper/FAKE_LOOP1p1 /fake/mnt', output_command: false)
54
+
55
+ disk_image.mount
56
+ end
57
+
58
+ context 'when the device does not exist' do
59
+ let(:mount_command) do
60
+ 'sudo mount /dev/mapper/FAKE_LOOP1p1 /fake/mnt'
61
+ end
62
+
63
+ let(:mount_error) do
64
+ "Failed: '#{mount_command}' from /fake/mnt/blah/blah/bosh-stemcell, with exit status 8192\n\n"
65
+ end
66
+
67
+ before do
68
+ disk_image.stub(:sleep)
69
+ end
70
+
71
+ it 'runs mount a second time after sleeping long enough for the device node to be created' do
72
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
73
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
74
+ shell.stub(:run).with('sudo kpartx -av /dev/loop0', output_command: false).and_return(kpartx_map_output)
75
+ shell.should_receive(:run).with(mount_command, output_command: false).ordered.and_raise(mount_error)
76
+ disk_image.should_receive(:sleep).with(0.5)
77
+ shell.should_receive(:run).with(mount_command, output_command: false).ordered
78
+
79
+ disk_image.mount
80
+ end
81
+
82
+ context 'when the second mount command fails' do
83
+ it 'raises an error' do
84
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
85
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
86
+ shell.stub(:run).with('sudo kpartx -av /dev/loop0', output_command: false).and_return(kpartx_map_output)
87
+ shell.should_receive(:run).with(mount_command, output_command: false).ordered.twice.and_raise(mount_error)
88
+
89
+ expect { disk_image.mount }.to raise_error(mount_error)
90
+ end
91
+ end
92
+ end
93
+
94
+ context 'when the mount command fails' do
95
+ it 'runs mount a second time' do
96
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
97
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
98
+ shell.stub(:run).with('sudo kpartx -av /dev/loop0', output_command: false).and_return(kpartx_map_output)
99
+ shell.should_receive(:run).
100
+ with('sudo mount /dev/mapper/FAKE_LOOP1p1 /fake/mnt', output_command: false).ordered.
101
+ and_raise(RuntimeError, 'UNEXEPECTED')
102
+
103
+ expect { disk_image.mount }.to raise_error(RuntimeError, 'UNEXEPECTED')
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '#unmount' do
109
+ before do
110
+ disk_image.stub(device: '/dev/loop0') # pretend we've mounted
111
+ end
112
+
113
+ it 'unmounts the loop device and then unmaps the file' do
114
+ shell.should_receive(:run).with('sudo umount /fake/mnt', output_command: false).ordered
115
+ shell.should_receive(:run).with('sudo kpartx -dv /dev/loop0', output_command: false).ordered
116
+ shell.should_receive(:run).with('sudo losetup -dv /dev/loop0', output_command: false).ordered
117
+
118
+ disk_image.unmount
119
+ end
120
+
121
+ it 'unmaps the file even if unmounting the device fails' do
122
+ shell.should_receive(:run).with('sudo umount /fake/mnt', output_command: false).and_raise
123
+ shell.should_receive(:run).with('sudo kpartx -dv /dev/loop0', output_command: false).ordered
124
+ shell.should_receive(:run).with('sudo losetup -dv /dev/loop0', output_command: false).ordered
125
+
126
+ expect { disk_image.unmount }.to raise_error
127
+ end
128
+ end
129
+
130
+ describe '#while_mounted' do
131
+ it 'mounts the disk, calls the provided block, and unmounts' do
132
+ fake_thing = double('FakeThing')
133
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
134
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
135
+ shell.stub(:run).with('sudo kpartx -av /dev/loop0', output_command: false).and_return(kpartx_map_output)
136
+ shell.should_receive(:run).with('sudo mount /dev/mapper/FAKE_LOOP1p1 /fake/mnt', output_command: false)
137
+ fake_thing.should_receive(:fake_call).with(disk_image).ordered
138
+ shell.should_receive(:run).with('sudo umount /fake/mnt', output_command: false).ordered
139
+ shell.should_receive(:run).with('sudo kpartx -dv /dev/loop0', output_command: false).ordered
140
+ shell.should_receive(:run).with('sudo losetup -dv /dev/loop0', output_command: false).ordered
141
+
142
+ disk_image.while_mounted do |image|
143
+ fake_thing.fake_call(image)
144
+ end
145
+ end
146
+
147
+ context 'when the block raises and error' do
148
+ it 'mounts the disk, calls the provided block, and unmounts' do
149
+ losetup_commad = 'sudo losetup --show --find /path/to/FAKE_IMAGE'
150
+ shell.stub(:run).with(losetup_commad, output_command: false).and_return('/dev/loop0')
151
+ shell.stub(:run).with('sudo kpartx -av /dev/loop0', output_command: false).and_return(kpartx_map_output)
152
+ shell.should_receive(:run).with('sudo mount /dev/mapper/FAKE_LOOP1p1 /fake/mnt', output_command: false)
153
+
154
+ shell.should_receive(:run).with('sudo umount /fake/mnt', output_command: false).ordered
155
+ shell.should_receive(:run).with('sudo kpartx -dv /dev/loop0', output_command: false).ordered
156
+ shell.should_receive(:run).with('sudo losetup -dv /dev/loop0', output_command: false).ordered
157
+
158
+ expect { disk_image.while_mounted { |_| raise } }.to raise_error
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+ require 'bosh/stemcell/infrastructure'
3
+
4
+ module Bosh::Stemcell
5
+ describe Infrastructure do
6
+ describe '.for' do
7
+ it 'returns the correct infrastrcture' do
8
+ expect(Infrastructure.for('openstack')).to be_an(Infrastructure::OpenStack)
9
+ expect(Infrastructure.for('aws')).to be_an(Infrastructure::Aws)
10
+ expect(Infrastructure.for('vsphere')).to be_a(Infrastructure::Vsphere)
11
+ end
12
+
13
+ it 'raises for unknown instructures' do
14
+ expect {
15
+ Infrastructure.for('BAD_INFRASTRUCTURE')
16
+ }.to raise_error(ArgumentError, /invalid infrastructure: BAD_INFRASTRUCTURE/)
17
+ end
18
+ end
19
+ end
20
+
21
+ describe Infrastructure::Base do
22
+ it 'requires a name to be specified' do
23
+ expect {
24
+ Infrastructure::Base.new
25
+ }.to raise_error /key not found: :name/
26
+ end
27
+
28
+ it 'requires a hypervisor' do
29
+ expect {
30
+ Infrastructure::Base.new(name: 'foo', default_disk_size: 1024)
31
+ }.to raise_error /key not found: :hypervisor/
32
+ end
33
+
34
+ it 'requires a default_disk_size' do
35
+ expect {
36
+ Infrastructure::Base.new(name: 'foo', hypervisor: 'xen')
37
+ }.to raise_error /key not found: :default_disk_size/
38
+ end
39
+
40
+ it 'defaults to not supporting light stemcells' do
41
+ infrastructure = Infrastructure::Base.new(name: 'foo', hypervisor: 'bar', default_disk_size: 1024)
42
+ expect(infrastructure).not_to be_light
43
+ end
44
+ end
45
+
46
+ describe Infrastructure::Aws do
47
+ its(:name) { should eq('aws') }
48
+ its(:hypervisor) { should eq('xen') }
49
+ its(:default_disk_size) { should eq(2048) }
50
+ it { should be_light }
51
+ end
52
+
53
+ describe Infrastructure::OpenStack do
54
+ its(:name) { should eq('openstack') }
55
+ its(:hypervisor) { should eq('kvm') }
56
+ its(:default_disk_size) { should eq(10240) }
57
+ it { should_not be_light }
58
+ end
59
+
60
+ describe Infrastructure::Vsphere do
61
+ its(:name) { should eq('vsphere') }
62
+ its(:hypervisor) { should eq('esxi') }
63
+ its(:default_disk_size) { should eq(2048) }
64
+ it { should_not be_light }
65
+ end
66
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'bosh/stemcell/operating_system'
3
+
4
+ module Bosh::Stemcell
5
+ describe OperatingSystem do
6
+ describe '.for' do
7
+ it 'returns the correct infrastrcture' do
8
+ expect(OperatingSystem.for('centos')).to be_a(OperatingSystem::Centos)
9
+ expect(OperatingSystem.for('ubuntu')).to be_a(OperatingSystem::Ubuntu)
10
+ end
11
+
12
+ it 'raises for unknown operating system' do
13
+ expect {
14
+ OperatingSystem.for('BAD_OPERATING_SYSTEM')
15
+ }.to raise_error(ArgumentError, /invalid operating system: BAD_OPERATING_SYSTEM/)
16
+ end
17
+ end
18
+ end
19
+
20
+ describe OperatingSystem::Base do
21
+ describe '#initialize' do
22
+ it 'requires :name to be specified' do
23
+ expect {
24
+ OperatingSystem::Base.new
25
+ }.to raise_error /key not found: :name/
26
+ end
27
+ end
28
+
29
+ describe '#name' do
30
+ subject { OperatingSystem::Base.new(name: 'CLOUDY_PONY_OS') }
31
+
32
+ its(:name) { should eq('CLOUDY_PONY_OS') }
33
+ end
34
+ end
35
+
36
+ describe OperatingSystem::Centos do
37
+ subject { OperatingSystem::Centos.new }
38
+
39
+ its(:name) { should eq('centos') }
40
+ end
41
+
42
+ describe OperatingSystem::Ubuntu do
43
+ subject { OperatingSystem::Ubuntu.new }
44
+
45
+ its(:name) { should eq('ubuntu') }
46
+ end
47
+ end