vcloud-box-spinner 0.2.0

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.
@@ -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