vcloud-launcher 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +16 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +127 -0
- data/Rakefile +47 -0
- data/bin/vcloud-launch +47 -0
- data/examples/.fog-example.fog +15 -0
- data/examples/vcloud-launch/basic_preamble.erb +8 -0
- data/examples/vcloud-launch/complete_vapp_config.yaml +58 -0
- data/examples/vcloud-launch/minimal_vapp_config.yaml +11 -0
- data/examples/vcloud-launch/multiple_vapps_simple.yaml +45 -0
- data/examples/vcloud-launch/yaml_anchors_example.yaml +87 -0
- data/features/step_definitions/vcloud-launch_steps.rb +3 -0
- data/features/support/env.rb +16 -0
- data/features/vcloud-launch.feature +16 -0
- data/jenkins.sh +11 -0
- data/jenkins_integration_tests.sh +7 -0
- data/lib/vcloud/launcher.rb +22 -0
- data/lib/vcloud/launcher/launch.rb +46 -0
- data/lib/vcloud/launcher/vapp_orchestrator.rb +44 -0
- data/lib/vcloud/launcher/version.rb +5 -0
- data/lib/vcloud/launcher/vm_orchestrator.rb +85 -0
- data/scripts/basic.erb +13 -0
- data/scripts/generate_fog_conf_file.sh +6 -0
- data/spec/erb_helper.rb +11 -0
- data/spec/integration/launcher/data/basic_preamble_test.erb +8 -0
- data/spec/integration/launcher/data/happy_path.yaml.erb +32 -0
- data/spec/integration/launcher/data/minimum_data_setup.yaml.erb +6 -0
- data/spec/integration/launcher/data/storage_profile.yaml.erb +24 -0
- data/spec/integration/launcher/storage_profile_integration_spec.rb +90 -0
- data/spec/integration/launcher/vcloud_launcher_spec.rb +157 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/stub_fog_interface.rb +59 -0
- data/spec/vcloud/launcher/launch_spec.rb +48 -0
- data/spec/vcloud/launcher/vapp_orchestrator_spec.rb +57 -0
- data/spec/vcloud/launcher/vm_orchestrator_spec.rb +80 -0
- data/vcloud-launcher.gemspec +30 -0
- metadata +214 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
require 'methadone/cucumber'
|
3
|
+
|
4
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
5
|
+
LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
|
6
|
+
|
7
|
+
Before do
|
8
|
+
# Using "announce" causes massive warnings on 1.9.2
|
9
|
+
@puts = true
|
10
|
+
@original_rubylib = ENV['RUBYLIB']
|
11
|
+
ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
After do
|
15
|
+
ENV['RUBYLIB'] = @original_rubylib
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Feature: "vcloud-launch" works as a useful command-line tool
|
2
|
+
In order to use "vcloud-launch" from the CLI
|
3
|
+
I want to have it behave like a typical Unix tool
|
4
|
+
So I don't get surpised
|
5
|
+
|
6
|
+
Scenario: Common arguments work
|
7
|
+
When I get help for "vcloud-launch"
|
8
|
+
Then the exit status should be 0
|
9
|
+
And the banner should be present
|
10
|
+
And the banner should document that this app takes options
|
11
|
+
And the following options should be documented:
|
12
|
+
|--version|
|
13
|
+
|--dont-power-on|
|
14
|
+
|--continue-on-error|
|
15
|
+
And the banner should document that this app's arguments are:
|
16
|
+
|org_config_file|
|
data/jenkins.sh
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/bash -x
|
2
|
+
set -e
|
3
|
+
bundle install --path "${HOME}/bundles/${JOB_NAME}"
|
4
|
+
bundle exec rake
|
5
|
+
|
6
|
+
./scripts/generate_fog_conf_file.sh
|
7
|
+
export FOG_RC=fog_integration_test.config
|
8
|
+
bundle exec rake integration:all
|
9
|
+
rm fog_integration_test.config
|
10
|
+
|
11
|
+
bundle exec rake publish_gem
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'vcloud/fog'
|
2
|
+
require 'vcloud/core'
|
3
|
+
|
4
|
+
require 'vcloud/launcher/launch'
|
5
|
+
require 'vcloud/launcher/vm_orchestrator'
|
6
|
+
require 'vcloud/launcher/vapp_orchestrator'
|
7
|
+
|
8
|
+
require 'vcloud/launcher/version'
|
9
|
+
|
10
|
+
module Vcloud
|
11
|
+
module Launcher
|
12
|
+
|
13
|
+
def self.logger
|
14
|
+
@logger ||= Logger.new(STDOUT)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.clone_object object
|
18
|
+
Marshal.load(Marshal.dump(object))
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Launcher
|
3
|
+
class Launch
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@config_loader = Vcloud::Core::ConfigLoader.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(config_file = nil, cli_options = {})
|
10
|
+
config = @config_loader.load_config(config_file, config_schema)
|
11
|
+
config[:vapps].each do |vapp_config|
|
12
|
+
Vcloud::Launcher.logger.info("\n")
|
13
|
+
Vcloud::Launcher.logger.info("Provisioning vApp #{vapp_config[:name]}.")
|
14
|
+
begin
|
15
|
+
vapp = ::Vcloud::Launcher::VappOrchestrator.provision(vapp_config)
|
16
|
+
#methadone sends option starting with 'no' as false.
|
17
|
+
vapp.power_on unless cli_options["dont-power-on"]
|
18
|
+
Vcloud::Launcher.logger.info("Done! Provisioned vApp #{vapp_config[:name]} successfully.")
|
19
|
+
Vcloud::Launcher.logger.info("=" * 70)
|
20
|
+
rescue RuntimeError => e
|
21
|
+
Vcloud::Launcher.logger.error("Failure : Could not provision vApp: #{e.message}")
|
22
|
+
Vcloud::Launcher.logger.info("=" * 70)
|
23
|
+
break unless cli_options["continue-on-error"]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def config_schema
|
30
|
+
{
|
31
|
+
type: 'hash',
|
32
|
+
allowed_empty: false,
|
33
|
+
permit_unknown_parameters: true,
|
34
|
+
internals: {
|
35
|
+
vapps: {
|
36
|
+
type: 'array',
|
37
|
+
required: false,
|
38
|
+
allowed_empty: true,
|
39
|
+
each_element_is: ::Vcloud::Launcher::VappOrchestrator.provision_schema
|
40
|
+
},
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Launcher
|
3
|
+
class VappOrchestrator
|
4
|
+
|
5
|
+
def self.provision(vapp_config)
|
6
|
+
name, vdc_name = vapp_config[:name], vapp_config[:vdc_name]
|
7
|
+
|
8
|
+
if vapp = Vcloud::Core::Vapp.get_by_name_and_vdc_name(name, vdc_name)
|
9
|
+
Vcloud::Launcher.logger.info("Found existing vApp #{name} in vDC '#{vdc_name}'. Skipping.")
|
10
|
+
else
|
11
|
+
template = Vcloud::Core::VappTemplate.get(vapp_config[:catalog], vapp_config[:catalog_item])
|
12
|
+
template_id = template.id
|
13
|
+
|
14
|
+
network_names = extract_vm_networks(vapp_config)
|
15
|
+
vapp = Vcloud::Core::Vapp.instantiate(name, network_names, template_id, vdc_name)
|
16
|
+
Vcloud::Launcher::VmOrchestrator.new(vapp.fog_vms.first, vapp).customize(vapp_config[:vm]) if vapp_config[:vm]
|
17
|
+
end
|
18
|
+
vapp
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.provision_schema
|
22
|
+
{
|
23
|
+
type: 'hash',
|
24
|
+
required: true,
|
25
|
+
allowed_empty: false,
|
26
|
+
internals: {
|
27
|
+
name: { type: 'string', required: true, allowed_empty: false },
|
28
|
+
vdc_name: { type: 'string', required: true, allowed_empty: false },
|
29
|
+
catalog: { type: 'string', required: true, allowed_empty: false },
|
30
|
+
catalog_item: { type: 'string', required: true, allowed_empty: false },
|
31
|
+
vm: Vcloud::Launcher::VmOrchestrator.customize_schema,
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.extract_vm_networks(config)
|
37
|
+
if (config[:vm] && config[:vm][:network_connections])
|
38
|
+
config[:vm][:network_connections].collect { |h| h[:name] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Vcloud
|
2
|
+
module Launcher
|
3
|
+
class VmOrchestrator
|
4
|
+
def initialize fog_vm, vapp
|
5
|
+
vm_id = fog_vm[:href].split('/').last
|
6
|
+
@vm = Core::Vm.new(vm_id, vapp)
|
7
|
+
end
|
8
|
+
|
9
|
+
def customize(vm_config)
|
10
|
+
@vm.update_name(@vm.vapp_name)
|
11
|
+
@vm.configure_network_interfaces vm_config[:network_connections]
|
12
|
+
@vm.update_storage_profile(vm_config[:storage_profile]) if vm_config[:storage_profile]
|
13
|
+
if hardware_config = vm_config[:hardware_config]
|
14
|
+
@vm.update_cpu_count(hardware_config[:cpu])
|
15
|
+
@vm.update_memory_size_in_mb(hardware_config[:memory])
|
16
|
+
end
|
17
|
+
@vm.add_extra_disks(vm_config[:extra_disks])
|
18
|
+
@vm.update_metadata(vm_config[:metadata])
|
19
|
+
@vm.configure_guest_customization_section(
|
20
|
+
@vm.vapp_name,
|
21
|
+
vm_config[:bootstrap],
|
22
|
+
vm_config[:extra_disks]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.customize_schema
|
27
|
+
{
|
28
|
+
type: 'hash',
|
29
|
+
required: false,
|
30
|
+
allowed_empty: false,
|
31
|
+
internals: {
|
32
|
+
network_connections: {
|
33
|
+
type: 'array',
|
34
|
+
required: false,
|
35
|
+
each_element_is: {
|
36
|
+
type: 'hash',
|
37
|
+
internals: {
|
38
|
+
name: { type: 'string', required: true },
|
39
|
+
ip_address: { type: 'ip_address', required: false },
|
40
|
+
},
|
41
|
+
},
|
42
|
+
},
|
43
|
+
storage_profile: { type: 'string', required: false },
|
44
|
+
hardware_config: {
|
45
|
+
type: 'hash',
|
46
|
+
required: false,
|
47
|
+
internals: {
|
48
|
+
cpu: { type: 'string_or_number', required: false },
|
49
|
+
memory: { type: 'string_or_number', required: false },
|
50
|
+
},
|
51
|
+
},
|
52
|
+
extra_disks: {
|
53
|
+
type: 'array',
|
54
|
+
required: false,
|
55
|
+
allowed_empty: false,
|
56
|
+
each_element_is: {
|
57
|
+
type: 'hash',
|
58
|
+
internals: {
|
59
|
+
name: { type: 'string', required: false },
|
60
|
+
size: { type: 'string_or_number', required: false },
|
61
|
+
},
|
62
|
+
},
|
63
|
+
},
|
64
|
+
bootstrap: {
|
65
|
+
type: 'hash',
|
66
|
+
required: false,
|
67
|
+
allowed_empty: false,
|
68
|
+
internals: {
|
69
|
+
script_path: { type: 'string', required: false },
|
70
|
+
script_post_processor: { type: 'string', required: false },
|
71
|
+
vars: { type: 'hash', required: false, allowed_empty: true },
|
72
|
+
},
|
73
|
+
},
|
74
|
+
metadata: {
|
75
|
+
type: 'hash',
|
76
|
+
required: false,
|
77
|
+
allowed_empty: true,
|
78
|
+
},
|
79
|
+
},
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/scripts/basic.erb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
(
|
4
|
+
echo "============================================================"
|
5
|
+
echo "in ${1}:"
|
6
|
+
echo "vapp_name: <%= vapp_name -%>"
|
7
|
+
echo "role: <%= vars[:role] -%>"
|
8
|
+
echo "environment: <%= vars[:environment] -%>"
|
9
|
+
echo
|
10
|
+
env
|
11
|
+
) >> /PREAMBLE_OUT
|
12
|
+
|
13
|
+
|
data/spec/erb_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
class ErbHelper
|
2
|
+
def self.convert_erb_template_to_yaml test_namespace, input_erb_config
|
3
|
+
input_erb_config = input_erb_config
|
4
|
+
e = ERB.new(File.open(input_erb_config).read)
|
5
|
+
output_yaml_config = File.join(File.dirname(input_erb_config), "output_#{Time.now.strftime('%s')}.yaml")
|
6
|
+
File.open(output_yaml_config, 'w') { |f|
|
7
|
+
f.write e.result(OpenStruct.new(test_namespace).instance_eval { binding })
|
8
|
+
}
|
9
|
+
output_yaml_config
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
---
|
2
|
+
vapps:
|
3
|
+
- name: <%= vapp_name %>
|
4
|
+
vdc_name: <%= vdc_name %>
|
5
|
+
catalog: <%= catalog %>
|
6
|
+
catalog_item: <%= vapp_template %>
|
7
|
+
vm:
|
8
|
+
hardware_config:
|
9
|
+
memory: 8192
|
10
|
+
cpu: 4
|
11
|
+
metadata:
|
12
|
+
is_integer: -999
|
13
|
+
is_string: Hello World
|
14
|
+
is_datetime: <%= date_metadata %>
|
15
|
+
is_true: true
|
16
|
+
is_false: false
|
17
|
+
integration_test_vm: true
|
18
|
+
extra_disks:
|
19
|
+
- size: '1024'
|
20
|
+
name: Hard disk 2
|
21
|
+
- size: '2048'
|
22
|
+
name: Hard disk 3
|
23
|
+
network_connections:
|
24
|
+
- name: <%= network1 %>
|
25
|
+
ip_address: <%= network1_ip %>
|
26
|
+
- name: <%= network2 %>
|
27
|
+
ip_address: <%= network2_ip %>
|
28
|
+
bootstrap:
|
29
|
+
script_path: <%= bootstrap_script %>
|
30
|
+
vars:
|
31
|
+
message: hello world
|
32
|
+
storage_profile: <%= storage_profile %>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
---
|
2
|
+
vapps:
|
3
|
+
- name: <%= vapp_name_1 %>
|
4
|
+
vdc_name: <%= vdc_name_1 %>
|
5
|
+
catalog: <%= catalog %>
|
6
|
+
catalog_item: <%= vapp_template %>
|
7
|
+
vm:
|
8
|
+
storage_profile: <%= storage_profile %>
|
9
|
+
- name: <%= vapp_name_2 %>
|
10
|
+
vdc_name: <%= vdc_name_2 %>
|
11
|
+
catalog: <%= catalog %>
|
12
|
+
catalog_item: <%= vapp_template %>
|
13
|
+
vm:
|
14
|
+
storage_profile: <%= storage_profile %>
|
15
|
+
- name: <%= vapp_name_3 %>
|
16
|
+
vdc_name: <%= vdc_name_1 %>
|
17
|
+
catalog: <%= catalog %>
|
18
|
+
catalog_item: <%= vapp_template %>
|
19
|
+
- name: <%= vapp_name_4 %>
|
20
|
+
vdc_name: <%= vdc_name_1 %>
|
21
|
+
catalog: <%= catalog %>
|
22
|
+
catalog_item: <%= vapp_template %>
|
23
|
+
vm:
|
24
|
+
storage_profile: <%= nonsense_storage_profile %>
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Vcloud::Launcher::Launch do
|
4
|
+
context "storage profile", :take_too_long => true do
|
5
|
+
before(:all) do
|
6
|
+
@test_data = define_test_data
|
7
|
+
@config_yaml = ErbHelper.convert_erb_template_to_yaml(@test_data, File.join(File.dirname(__FILE__), 'data/storage_profile.yaml.erb'))
|
8
|
+
@fog_interface = Vcloud::Fog::ServiceInterface.new
|
9
|
+
Vcloud::Launcher::Launch.new.run(@config_yaml, {'dont-power-on' => true})
|
10
|
+
|
11
|
+
@vapp_query_result_1 = @fog_interface.get_vapp_by_name_and_vdc_name(@test_data[:vapp_name_1], @test_data[:vdc_name_1])
|
12
|
+
@vapp_id_1 = @vapp_query_result_1[:href].split('/').last
|
13
|
+
@vapp_1 = @fog_interface.get_vapp @vapp_id_1
|
14
|
+
@vm_1 = @vapp_1[:Children][:Vm].first
|
15
|
+
|
16
|
+
@vapp_query_result_2 = @fog_interface.get_vapp_by_name_and_vdc_name(@test_data[:vapp_name_2], @test_data[:vdc_name_2])
|
17
|
+
@vapp_id_2 = @vapp_query_result_2[:href].split('/').last
|
18
|
+
@vapp_2 = @fog_interface.get_vapp @vapp_id_2
|
19
|
+
@vm_2 = @vapp_2[:Children][:Vm].first
|
20
|
+
|
21
|
+
@vapp_query_result_3 = @fog_interface.get_vapp_by_name_and_vdc_name(@test_data[:vapp_name_3], @test_data[:vdc_name_1])
|
22
|
+
@vapp_id_3 = @vapp_query_result_3[:href].split('/').last
|
23
|
+
@vapp_3 = @fog_interface.get_vapp @vapp_id_3
|
24
|
+
@vm_3 = @vapp_3[:Children][:Vm].first
|
25
|
+
|
26
|
+
@vapp_query_result_4 = @fog_interface.get_vapp_by_name_and_vdc_name(@test_data[:vapp_name_4], @test_data[:vdc_name_1])
|
27
|
+
@vapp_id_4 = @vapp_query_result_4[:href].split('/').last
|
28
|
+
@vapp_4 = @fog_interface.get_vapp @vapp_id_4
|
29
|
+
@vm_4 = @vapp_4[:Children][:Vm].first
|
30
|
+
end
|
31
|
+
|
32
|
+
it "vdc 1 should have a storage profile without the href being specified" do
|
33
|
+
@vm_1[:StorageProfile][:name].should == @test_data[:storage_profile]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "vdc 1's storage profile should have the expected href" do
|
37
|
+
@vm_1[:StorageProfile][:href].should == @test_data[:vdc_1_sp_href]
|
38
|
+
end
|
39
|
+
|
40
|
+
it "vdc 2 should have the same named storage profile as vdc 1" do
|
41
|
+
@vm_2[:StorageProfile][:name].should == @test_data[:storage_profile]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "the storage profile in vdc 2 should have a different href to the storage profile in vdc 1" do
|
45
|
+
@vm_2[:StorageProfile][:href].should == @test_data[:vdc_2_sp_href]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "when a storage profile is not specified, vm uses the default and continues" do
|
49
|
+
@vm_3[:StorageProfile][:name].should == @test_data[:default_storage_profile_name]
|
50
|
+
@vm_3[:StorageProfile][:href].should == @test_data[:default_storage_profile_href]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "when a storage profile specified does not exist, vm uses the default" do
|
54
|
+
@vm_4[:StorageProfile][:name].should == @test_data[:default_storage_profile_name]
|
55
|
+
@vm_4[:StorageProfile][:href].should == @test_data[:default_storage_profile_href]
|
56
|
+
end
|
57
|
+
|
58
|
+
after(:all) do
|
59
|
+
unless ENV['VCLOUD_TOOLS_RSPEC_NO_DELETE_VAPP']
|
60
|
+
File.delete @config_yaml
|
61
|
+
@fog_interface.delete_vapp(@vapp_id_1).should == true
|
62
|
+
@fog_interface.delete_vapp(@vapp_id_2).should == true
|
63
|
+
@fog_interface.delete_vapp(@vapp_id_3).should == true
|
64
|
+
@fog_interface.delete_vapp(@vapp_id_4).should == true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
def define_test_data
|
73
|
+
{
|
74
|
+
vapp_name_1: "vdc-1-sp-#{Time.now.strftime('%s')}",
|
75
|
+
vapp_name_2: "vdc-2-sp-#{Time.now.strftime('%s')}",
|
76
|
+
vapp_name_3: "vdc-3-sp-#{Time.now.strftime('%s')}",
|
77
|
+
vapp_name_4: "vdc-4-sp-#{Time.now.strftime('%s')}",
|
78
|
+
vdc_name_1: ENV['VDC_NAME_1'],
|
79
|
+
vdc_name_2: ENV['VDC_NAME_2'],
|
80
|
+
catalog: ENV['VCLOUD_CATALOG_NAME'],
|
81
|
+
vapp_template: ENV['VCLOUD_TEMPLATE_NAME'],
|
82
|
+
storage_profile: ENV['VCLOUD_STORAGE_PROFILE_NAME'],
|
83
|
+
vdc_1_sp_href: ENV['VDC_1_STORAGE_PROFILE_HREF'],
|
84
|
+
vdc_2_sp_href: ENV['VDC_2_STORAGE_PROFILE_HREF'],
|
85
|
+
default_storage_profile_name: ENV['DEFAULT_STORAGE_PROFILE_NAME'],
|
86
|
+
default_storage_profile_href: ENV['DEFAULT_STORAGE_PROFILE_HREF'],
|
87
|
+
nonsense_storage_profile: "nonsense-storage-profile-name",
|
88
|
+
bootstrap_script: File.join(File.dirname(__FILE__), "data/basic_preamble_test.erb"),
|
89
|
+
}
|
90
|
+
end
|