vcloud-box-spinner 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ module Provisioner
2
+ module ComputeAction
3
+ module Delete
4
+
5
+ def delete_vapp
6
+ super
7
+ vapp_href = compute.servers.service.vapps.detect {|v| v.name == options[:vm_name] }.href
8
+ vapp = compute.servers.service.get_vapp(vapp_href)
9
+ if vapp.on? or (vapp.off? and vapp.deployed)
10
+ logger.debug "The vApp is running, stopping it..."
11
+ vapp.service.undeploy vapp_href
12
+ logger.debug "Waiting for vApp to stop ..."
13
+ vapp.wait_for { vapp.off? }
14
+ end
15
+ vapp.wait_for { vapp.off? } #double check
16
+
17
+ # This is added as vapp after being off, might still
18
+ # be in a state which isn't ready. This would check
19
+ # status of each vm associated with vapp
20
+ vapp.servers.entries.each { |server| server.wait_for { server.ready? } }
21
+
22
+ logger.debug "The vApp is not running now ..."
23
+ logger.debug "Deleting the vApp"
24
+ vapp.service.delete_vapp vapp_href
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ require 'cgi'
2
+ require 'builder'
3
+ require 'fog/vcloud/compute/shared' # our hack
4
+ require 'fog/vcloud/compute/server_ready' # another hack
5
+ require 'nokogiri'
6
+ require 'provisioner/compute_action/create'
7
+ require 'provisioner/compute_action/delete'
8
+
9
+ module Provisioner
10
+ module ComputeNode
11
+ include ComputeAction::Create
12
+ include ComputeAction::Delete
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ module Provisioner
2
+ # TODO: this should be some GDS error class
3
+ class ProvisionerError < RuntimeError; end
4
+ class ConfigurationError < ProvisionerError; end
5
+ class NoSuchRole < ProvisionerError; end
6
+ end
@@ -0,0 +1,134 @@
1
+ module Provisioner
2
+ class Provisioner
3
+ AVAILABLE_ACTIONS = ['create', 'delete']
4
+
5
+ attr_accessor :options
6
+ private :options=, :options
7
+
8
+ def initialize options
9
+ options[:provider] = 'vcloud'
10
+ options[:created_by] = ENV['USER']
11
+ self.options = options
12
+ end
13
+
14
+ def execute(action)
15
+ unless AVAILABLE_ACTIONS.include?(action)
16
+ raise(ConfigurationError, "The action '#{action}' is not a valid action")
17
+ end
18
+ send(action)
19
+ end
20
+
21
+ def create
22
+ logger.debug "Validating options"
23
+ validate_options
24
+ logger.debug "Preparing the run"
25
+ prepare_run
26
+ logger.debug "Launching servers"
27
+ launch_servers
28
+ logger.debug "Done"
29
+ end
30
+ private :create
31
+
32
+ def delete
33
+ logger.debug "Validating options"
34
+ validate_options
35
+ if ask("Do you really want to delete '#{options[:vm_name]}'? (yes/no) ") == "yes"
36
+ logger.debug "Proceeding delete operation"
37
+ delete_vapp
38
+ else
39
+ logger.debug "Abandoning delete operation"
40
+ end
41
+ end
42
+ private :delete
43
+
44
+ def compute
45
+ @compute ||= Fog::Compute.new(
46
+ :provider => options[:provider],
47
+ :vcloud_username => "#{options[:user]}@#{options[:organisation]}",
48
+ :vcloud_password => options[:password],
49
+ :vcloud_host => options[:host],
50
+ :vcloud_default_vdc => options[:default_vdc],
51
+ :connection_options => {
52
+ :ssl_verify_peer => false,
53
+ :omit_default_port => true
54
+ }
55
+ )
56
+ end
57
+ private :compute
58
+
59
+ def logger
60
+ options[:logger]
61
+ end
62
+ private :logger
63
+
64
+ def ssh
65
+ @ssh ||= begin
66
+ Provisioner.ssh_client.tap { |client|
67
+ logger.debug "Using #{client} as my SSH client"
68
+ }
69
+ end
70
+ end
71
+
72
+ def ssh_to hostname, &blk
73
+ puts "Sshing to #{hostname}"
74
+ ssh.start hostname,
75
+ options[:ssh_user],
76
+ :config => options[:ssh_config],
77
+ &blk
78
+ end
79
+
80
+ def validate_options
81
+ unless options[:password] && options[:user] && options[:host]
82
+ logger.error "VCloud credentials missing"
83
+ raise ConfigurationError, "VCloud credentials must be specified"
84
+ end
85
+ end
86
+ private :validate_options
87
+
88
+ def timestamp
89
+ @timestamp ||= Time.now.utc.to_i.to_s(36).tap { |ts|
90
+ logger.debug "The base 36 timestamp for this run in #{ts}"
91
+ }
92
+ end
93
+ private :timestamp
94
+
95
+ def launch_server name
96
+ end
97
+ private :launch_server
98
+
99
+ def notify message, name
100
+ logger.info "<%s> %s" % [ name, message ]
101
+ end
102
+ private :notify
103
+
104
+ def launch_servers
105
+ Parallel.each(options[:num_servers].times, :in_threads => options[:num_servers]) do |number|
106
+ name = server_name number
107
+ server = launch_server name
108
+ bootstrap_server server, name
109
+ end
110
+ end
111
+ private :launch_servers
112
+
113
+ def server_name number
114
+ [
115
+ options[:platform],
116
+ timestamp,
117
+ ("%02d" % (number + 1))
118
+ ].compact.join('-')
119
+ end
120
+ private :server_name
121
+
122
+ def prepare_run
123
+ end
124
+ private :prepare_run
125
+
126
+ def bootstrap_server server, name
127
+ end
128
+ private :bootstrap_server
129
+
130
+ def delete_vapp
131
+ end
132
+ private
133
+ end
134
+ end
@@ -0,0 +1,3 @@
1
+ module Provisioner
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,31 @@
1
+ require 'provisioner/errors'
2
+ require 'provisioner/provisioner'
3
+ require 'provisioner/compute_node'
4
+ require 'provisioner/blank_provisioner'
5
+ require 'provisioner/cli'
6
+ require 'provisioner/version'
7
+ require 'socket'
8
+ require 'logger'
9
+ require 'net/ssh'
10
+ require 'net/scp'
11
+ require 'erb'
12
+ require 'parallel'
13
+ require 'fog'
14
+
15
+ module VcloudBoxProvisioner
16
+ def self.build options = {}
17
+ options[:logger] ||= default_logger options
18
+ options[:logger].debug "Building provisioner for #{options.inspect}"
19
+ Provisioner::BlankProvisioner.new options
20
+ end
21
+
22
+ class << self
23
+ attr_accessor :ssh_client
24
+ end
25
+ self.ssh_client = Net::SSH
26
+
27
+ def self.default_logger options
28
+ Logger.new(STDOUT).tap { |l| l.level = options[:log_level] || Logger::ERROR }
29
+ end
30
+
31
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'fog/vcloud/compute/shared'
3
+ require 'nokogiri'
4
+ require 'equivalent-xml'
5
+
6
+ describe Fog::Vcloud::Compute::Shared do
7
+ let (:fog_vcloud_test_class) do
8
+ Class.new do
9
+ include Fog::Vcloud::Compute::Shared
10
+
11
+ def xmlns
12
+ {}
13
+ end
14
+ end
15
+ end
16
+
17
+ it 'should generate InstantiateVAppTemplateParams' do
18
+ subject = fog_vcloud_test_class.new
19
+ fog_xml = subject.send(:generate_instantiate_vapp_template_request, {:name => 'foo',
20
+ :description => 'bar',
21
+ :template_uri => 'baz'
22
+ })
23
+ parsed = Nokogiri::XML(fog_xml)
24
+
25
+ xml = ::Builder::XmlMarkup.new
26
+ expected = xml.InstantiateVAppTemplateParams({:name => 'foo', :'xml:lang' => 'en'}) {
27
+ xml.Description('bar')
28
+ xml.InstantiationParams { }
29
+ xml.Source(:href => 'baz')
30
+ xml.AllEULAsAccepted("true")
31
+ }
32
+ parsed.should be_equivalent_to Nokogiri::XML(expected)
33
+ end
34
+
35
+ it 'should handle network_uri correctly' do
36
+ subject = fog_vcloud_test_class.new
37
+ fog_xml = subject.send(:generate_instantiate_vapp_template_request,
38
+ {:name => 'foo',
39
+ :description => 'bar',
40
+ :template_uri => 'baz',
41
+ :network_name => 'jimmy',
42
+ :network_uri => 'http://jimmy.invalid'
43
+ })
44
+
45
+ xml = ::Builder::XmlMarkup.new
46
+ expected = xml.InstantiateVAppTemplateParams({:name => 'foo', :'xml:lang' => 'en'}) {
47
+ xml.Description('bar')
48
+ xml.InstantiationParams {
49
+ xml.NetworkConfigSection {
50
+ xml.ovf :Info
51
+ xml.NetworkConfig(:networkName => 'jimmy') {
52
+ xml.Configuration {
53
+ xml.ParentNetwork(:href => 'http://jimmy.invalid')
54
+ xml.FenceMode 'bridged'
55
+ }
56
+ }
57
+ }
58
+ }
59
+ xml.Source(:href => 'baz')
60
+ xml.AllEULAsAccepted("true")
61
+ }
62
+ Nokogiri::XML(fog_xml).should be_equivalent_to Nokogiri::XML(expected)
63
+ end
64
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+ require 'provisioner/cli'
3
+
4
+ MACHINE_METADATA = {
5
+ :puppetmaster => 'foo.bar.baz',
6
+ :zone => 'foo',
7
+ :catalog_id => 'default_catalog_id',
8
+ }.freeze
9
+
10
+ DEFAULTS = {
11
+ :debug => false,
12
+ :log_level => 5,
13
+ :memory => 4096,
14
+ :num_cores => 2,
15
+ :num_servers => 1,
16
+ :platform => "production",
17
+ :ssh_config => true, # if not specified, use system defaults
18
+ }
19
+
20
+ describe Provisioner::CLI do
21
+ describe "#defaults" do
22
+ Provisioner::CLI.defaults.should == DEFAULTS
23
+ end
24
+
25
+ describe "#process" do
26
+ it 'should set options from built-in defaults' do
27
+ options = { :machine_metadata => MACHINE_METADATA,
28
+ :org_config => {} }
29
+ res = Provisioner::CLI.process(options)
30
+ res.should include(DEFAULTS)
31
+ end
32
+
33
+ it 'should set options from the vCloud config defaults' do
34
+ config = { :default => { :ip => '5.6.7.8' } }
35
+ res = Provisioner::CLI.process({:machine_metadata => MACHINE_METADATA,
36
+ :org_config => config})
37
+ res.should include(:ip => '5.6.7.8')
38
+ end
39
+
40
+ it 'should set options from the vCloud config based on the specified zone' do
41
+ config = { :foo => { :ip => '9.8.7.6' } }
42
+ res = Provisioner::CLI.process({:org_config => config,
43
+ :machine_metadata => MACHINE_METADATA})
44
+ res.should include(:ip => '9.8.7.6')
45
+ end
46
+
47
+ it 'should set options from the machine template' do
48
+ options = { :machine_metadata => MACHINE_METADATA.dup.merge( :ip => '1.2.3.4' ),
49
+ :org_config => {} }
50
+ res = Provisioner::CLI.process(options)
51
+ res.should include(:ip => '1.2.3.4')
52
+ end
53
+
54
+ it 'should set options from the command line' do
55
+ opts = {:ssh_user => 'donald',
56
+ :org_config => {},
57
+ :machine_metadata => MACHINE_METADATA }
58
+ res = Provisioner::CLI.process(opts)
59
+ res.should include(:ssh_user => 'donald')
60
+ end
61
+
62
+ it 'should override vCloud config defaults with vCloud zone defaults' do
63
+ config = {
64
+ :default => { :ip => '1.2.3.4' },
65
+ :foo => { :ip => '5.6.7.8' }
66
+ }
67
+ res = Provisioner::CLI.process({:org_config => config,
68
+ :machine_metadata => MACHINE_METADATA})
69
+ res.should include(:ip => '5.6.7.8')
70
+ end
71
+
72
+ it 'should override vCloud zone defaults with machine template options' do
73
+ options = { :org_config => { :foo => { :ip => '1.2.3.4' } },
74
+ :machine_metadata => MACHINE_METADATA.dup.merge(:ip => '5.6.7.8')}
75
+ res = Provisioner::CLI.process(options)
76
+ res.should include(:ip => '5.6.7.8')
77
+ end
78
+
79
+ it 'should override machine template options with command line options' do
80
+ options = { :machine_metadata => MACHINE_METADATA.dup.
81
+ merge(:ssh_user => 'aaron'),
82
+ :org_config => {},
83
+ :ssh_user => 'binky' }
84
+ res = Provisioner::CLI.process(options)
85
+ res.should include(:ssh_user => 'binky')
86
+ end
87
+
88
+ it 'should use catalog_id if present' do
89
+ options = { :machine_metadata => MACHINE_METADATA.dup.merge!(
90
+ :catalog_id => 'wibble',
91
+ :catalog_items => { :my_cool_template => 'incorrect' },
92
+ :template_name => 'my_cool_template'
93
+ ),
94
+ :org_config => {}}
95
+ res = Provisioner::CLI.process(options)
96
+ res.should include(:catalog_id => 'wibble')
97
+ end
98
+
99
+ it 'should use template_name to determine catalog_id if catalog_id not present' do
100
+ tpl = MACHINE_METADATA.dup
101
+ tpl.delete(:catalog_id)
102
+ tpl.merge!(
103
+ :catalog_items => { :my_cool_template => 'https://correct_catalog_id' },
104
+ :template_name => 'my_cool_template'
105
+ )
106
+ res = Provisioner::CLI.process({:machine_metadata => tpl,
107
+ :org_config => {}})
108
+ res.should include(:catalog_id => 'https://correct_catalog_id')
109
+ end
110
+
111
+ it 'should require a zone to be set' do
112
+ tpl = MACHINE_METADATA.dup
113
+ tpl.delete(:zone)
114
+ expect do
115
+ Provisioner::CLI.process({:machine_metadata => tpl})
116
+ end.to raise_error(Provisioner::ConfigurationError, /zone/)
117
+ end
118
+
119
+ end
120
+
121
+ describe "#execute" do
122
+ before :each do silence_output end
123
+ after :each do silence_output end
124
+
125
+ it "should fail if two arguments not provided" do
126
+ expect {
127
+ Provisioner::CLI.new(['-u' , 'user', '-p', 'pass']).execute
128
+ }.to raise_error(SystemExit)
129
+ end
130
+
131
+ it "should call provision a machine" do
132
+ default_org_config =
133
+ { :template_name => "template-name",
134
+ :host=>"api-end-host",
135
+ :platform=>"qa",
136
+ :organisation=>"org-name",
137
+ :catalog_items => {
138
+ :"template-name" => "https://vendor-api-endpoint/catalogItem/100"
139
+ }}
140
+ zone_org_config =
141
+ { :default_vdc=>"https://vendor-api-endpoint/api/vdc/07412",
142
+ :domain => "tester.default",
143
+ :network_name => "Default",
144
+ :network_uri => "https://vendor-api-endpoint/api/network/0352",
145
+ :vdc_id=>"07412" }
146
+ machine_metadata = { :zone => "tester",
147
+ :vm_name => "machine-2",
148
+ :ip => "192.168.2.2" }
149
+ expected_opts = DEFAULTS.merge(default_org_config).
150
+ merge(zone_org_config).
151
+ merge(machine_metadata).
152
+ merge({ :user => 'badger',
153
+ :password => 'eggplant',
154
+ :catalog_id => "https://vendor-api-endpoint/catalogItem/100"
155
+ })
156
+
157
+ VcloudBoxProvisioner.should_receive(:build).
158
+ with(expected_opts).
159
+ and_return(mock(:execute => true))
160
+
161
+ cli = Provisioner::CLI.new(['-u', 'badger', '-p', 'eggplant',
162
+ '-o', 'spec/test_data/org.json',
163
+ '-m', 'spec/test_data/machine.json',
164
+ 'create'
165
+ ])
166
+ cli.execute
167
+ end
168
+ end
169
+
170
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ require 'provisioner/compute_node'
3
+
4
+ module Provisioner
5
+ class DummyClass
6
+ include ComputeNode
7
+ end
8
+ end
9
+
10
+ describe 'Provisoner::ComputeNode' do
11
+
12
+ it "should undeploy the vApp is running while delete action" do
13
+ pending "mock not supported in fog"
14
+ end
15
+
16
+ it "should not undeploy the vApp is stopped while delete action" do
17
+ pending "mock not supported in fog"
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'provisioner/errors'
3
+ require 'provisioner/provisioner'
4
+
5
+ shared_examples "validate credentials" do |action|
6
+ it "should error if credentials not provided while perfoming #{action}" do
7
+ logger = mock(:debug => true, :error => true)
8
+ expect {
9
+ Provisioner::Provisioner.new({:logger => logger}).execute(action)
10
+ }.to raise_error(Provisioner::ConfigurationError,
11
+ 'VCloud credentials must be specified')
12
+ end
13
+ end
14
+
15
+ describe 'Provisioner::Provisioner' do
16
+
17
+ it_should_behave_like "validate credentials", 'create'
18
+ it_should_behave_like "validate credentials", 'delete'
19
+
20
+ it "should error out if action passed isn't present" do
21
+ logger = mock(:debug => true, :error => true)
22
+ expect {
23
+ Provisioner::Provisioner.new({:logger => logger}).execute('disco')
24
+ }.to raise_error(Provisioner::ConfigurationError,
25
+ 'The action \'disco\' is not a valid action')
26
+ end
27
+
28
+ describe "delete" do
29
+ it "delete vApp only if you confirm to delete vApp" do
30
+ pending "mock not supported in fog"
31
+ end
32
+ it "don't delete vApp only if you don't confirm to delete vApp" do
33
+ pending "mock not supported in fog"
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,23 @@
1
+ specdir=File.dirname(__FILE__)
2
+ $:.unshift File.join(specdir, '../lib')
3
+ $:.unshift File.join(specdir, '..')
4
+
5
+ require 'rspec'
6
+
7
+ # Redirects stderr and stdout to /dev/null.
8
+ def silence_output
9
+ @orig_stderr = $stderr
10
+ @orig_stdout = $stdout
11
+
12
+ # redirect stderr and stdout to /dev/null
13
+ $stderr = File.new('/dev/null', 'w')
14
+ $stdout = File.new('/dev/null', 'w')
15
+ end
16
+
17
+ # Replace stdout and stderr so anything else is output correctly.
18
+ def enable_output
19
+ $stderr = @orig_stderr
20
+ $stdout = @orig_stdout
21
+ @orig_stderr = nil
22
+ @orig_stdout = nil
23
+ end
@@ -0,0 +1,5 @@
1
+ {
2
+ "zone": "tester",
3
+ "vm_name": "machine-2",
4
+ "ip": "192.168.2.2"
5
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "default": {
3
+ "template_name": "template-name",
4
+ "host": "api-end-host",
5
+ "platform": "qa",
6
+ "organisation": "org-name",
7
+ "catalog_items": {
8
+ "template-name": "https://vendor-api-endpoint/catalogItem/100"
9
+ }
10
+ },
11
+ "tester": {
12
+ "default_vdc": "https://vendor-api-endpoint/api/vdc/07412",
13
+ "domain": "tester.default",
14
+ "network_name": "Default",
15
+ "network_uri": "https://vendor-api-endpoint/api/network/0352",
16
+ "vdc_id": "07412"
17
+ }
18
+ }
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "provisioner/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "vcloud-box-spinner"
7
+ s.version = Provisioner::VERSION
8
+ s.authors = ["Garima Singh"]
9
+ s.email = ["igarimasingh@gmail.com"]
10
+ s.homepage = "https://github.com/com/vcloud-box-spinner"
11
+ s.summary = %q{Provision servers, with vcloud API}
12
+ s.description = %q{Create new VM and apply an opinionated set of commands to
13
+ them, using vcloud API. The vcloud-box-spinner is a thin wrapper around fog,
14
+ which enables you to be able to configure VMs with static IPs}
15
+
16
+ s.rubyforge_project = "vcloud-box-spinner"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "minitest"
25
+ s.add_development_dependency "mocha"
26
+ s.add_development_dependency "webmock"
27
+ s.add_development_dependency "rspec", "~> 2.11.0"
28
+ s.add_development_dependency "equivalent-xml", "~> 0.2.9"
29
+ s.add_development_dependency "gem_publisher", "~> 1.3.0"
30
+ s.add_runtime_dependency "fog", "~> 1.0"
31
+ s.add_runtime_dependency "parallel"
32
+ s.add_runtime_dependency "highline"
33
+ s.add_runtime_dependency "nokogiri", "~> 1.5.0"
34
+ end