knife-vsphere 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,111 +1,167 @@
1
- #
2
- # Author:: Ezra Pagel (<ezra@cpan.org>)
3
- # License:: Apache License, Version 2.0
4
- #
5
-
6
- require 'chef/knife'
7
- require 'rbvmomi'
8
-
9
- # Base class for vsphere knife commands
10
- class Chef
11
- class Knife
12
- class BaseVsphereCommand < Knife
13
-
14
- deps do
15
- require 'chef/knife/bootstrap'
16
- Chef::Knife::Bootstrap.load_deps
17
- require 'fog'
18
- require 'socket'
19
- require 'net/ssh/multi'
20
- require 'readline'
21
- require 'chef/json_compat'
22
- end
23
-
24
-
25
- def self.get_common_options
26
-
27
- option :vsphere_user,
28
- :short => "-u USERNAME",
29
- :long => "--user USERNAME",
30
- :description => "The username for the host"
31
-
32
- option :password,
33
- :short => "-p PASSWORD",
34
- :long => "--password PASSWORD",
35
- :description => "The password for the host"
36
-
37
- option :datacenter,
38
- :short => "-d DATACENTER",
39
- :long => "--datacenter DATACENTER",
40
- :description => "The Datacenter to create the VM in"
41
-
42
- option :path,
43
- :long => "--path SOAP_PATH",
44
- :description => "The SOAP endpoint path",
45
- :proc => Proc.new { |p| Chef::Config[:knife][:path] = p },
46
- :default => "/sdk"
47
-
48
- option :port,
49
- :long => "--port PORT",
50
- :description => "The VI SDK port number to use",
51
- :proc => Proc.new { |p| Chef::Config[:knife][:port] = p },
52
- :default => 443
53
-
54
- option :use_ssl,
55
- :long => "--ssl USE_SSL",
56
- :description => "Whether to use SSL connection",
57
- :default => true
58
-
59
- option :insecure,
60
- :short => "-i USE_INSECURE_SSL",
61
- :long => "--insecure USE_INSECURE_SSL",
62
- :description => "Determines whether SSL certificate verification is skipped",
63
- :default => true
64
-
65
- end
66
-
67
- def get_vim_connection
68
-
69
- conn_opts = {
70
- :host => config[:host] || Chef::Config[:knife][:vsphere_host],
71
- :path => config[:path],
72
- :port => config[:port],
73
- :use_ssl => config[:ssl],
74
- :user => config[:vsphere_user] || Chef::Config[:knife][:vsphere_user],
75
- :password => config[:password] || Chef::Config[:knife][:vsphere_pass],
76
- :insecure => config[:insecure]
77
- }
78
-
79
- # opt :insecure, "don't verify ssl certificate", :short => 'k', :default => (ENV['RBVMOMI_INSECURE'] == '1')
80
- # opt :debug, "Log SOAP messages", :short => 'd', :default => (ENV['RBVMOMI_DEBUG'] || false)
81
-
82
- vim = RbVmomi::VIM.connect conn_opts
83
- return vim
84
- end
85
-
86
- def get_folders(folder)
87
- folder.childEntity.grep(RbVmomi::VIM::Folder) << folder
88
- end
89
-
90
- def find_all_in_folders(folder, type)
91
- get_folders(folder).
92
- collect { |f| f.childEntity.grep(type) }.
93
- flatten
94
- end
95
-
96
- def find_in_folders(folder, type, name)
97
- get_folders(folder).
98
- collect { |f| f.childEntity.grep(type) }.
99
- flatten.
100
- find { |o| o.name == name }
101
- end
102
-
103
- def fatal_exit(msg)
104
- ui.fatal(msg)
105
- exit 1
106
- end
107
-
108
-
109
- end
110
- end
111
- end
1
+ #
2
+ # Author:: Ezra Pagel (<ezra@cpan.org>)
3
+ # Contributor:: Jesse Campbell (<hikeit@gmail.com>)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+
7
+ require 'chef/knife'
8
+ require 'rbvmomi'
9
+
10
+ # Base class for vsphere knife commands
11
+ class Chef
12
+ class Knife
13
+ class BaseVsphereCommand < Knife
14
+
15
+ deps do
16
+ require 'chef/knife/bootstrap'
17
+ Chef::Knife::Bootstrap.load_deps
18
+ require 'fog'
19
+ require 'socket'
20
+ require 'net/ssh/multi'
21
+ require 'readline'
22
+ require 'chef/json_compat'
23
+ end
24
+
25
+
26
+ def self.get_common_options
27
+
28
+ option :vsphere_user,
29
+ :short => "-u USERNAME",
30
+ :long => "--user USERNAME",
31
+ :description => "The username for the host"
32
+
33
+ option :vsphere_pass,
34
+ :short => "-p PASSWORD",
35
+ :long => "--password PASSWORD",
36
+ :description => "The password for the host"
37
+
38
+ option :datacenter,
39
+ :short => "-d DATACENTER",
40
+ :long => "--datacenter DATACENTER",
41
+ :description => "The Datacenter to create the VM in"
42
+
43
+ option :path,
44
+ :long => "--path SOAP_PATH",
45
+ :description => "The SOAP endpoint path",
46
+ :proc => Proc.new { |p| Chef::Config[:knife][:path] = p },
47
+ :default => "/sdk"
48
+
49
+ option :port,
50
+ :long => "--port PORT",
51
+ :description => "The VI SDK port number to use",
52
+ :proc => Proc.new { |p| Chef::Config[:knife][:port] = p },
53
+ :default => 443
54
+
55
+ option :use_ssl,
56
+ :long => "--ssl USE_SSL",
57
+ :description => "Whether to use SSL connection",
58
+ :default => true
59
+
60
+ option :insecure,
61
+ :short => "-i USE_INSECURE_SSL",
62
+ :long => "--insecure USE_INSECURE_SSL",
63
+ :description => "Determines whether SSL certificate verification is skipped",
64
+ :default => true
65
+
66
+ option :folder,
67
+ :short => "-f FOLDER",
68
+ :long => "--folder FOLDER",
69
+ :description => "The folder to get VMs from",
70
+ :default => ''
71
+
72
+ end
73
+
74
+ def locate_config_value(key)
75
+ key = key.to_sym
76
+ Chef::Config[:knife][key] || config[key]
77
+ end
78
+
79
+ def get_vim_connection
80
+
81
+ conn_opts = {
82
+ :host => locate_config_value(:vsphere_host),
83
+ :path => config[:path],
84
+ :port => config[:port],
85
+ :use_ssl => config[:ssl],
86
+ :user => locate_config_value(:vsphere_user),
87
+ :password => locate_config_value(:vsphere_pass),
88
+ :insecure => config[:insecure]
89
+ }
90
+
91
+ # opt :insecure, "don't verify ssl certificate", :short => 'k', :default => (ENV['RBVMOMI_INSECURE'] == '1')
92
+ # opt :debug, "Log SOAP messages", :short => 'd', :default => (ENV['RBVMOMI_DEBUG'] || false)
93
+
94
+ vim = RbVmomi::VIM.connect conn_opts
95
+ config[:vim] = vim
96
+ return vim
97
+ end
98
+
99
+ def find_folder(folderName)
100
+ dcname = config[:vsphere_dc] || Chef::Config[:knife][:vsphere_dc]
101
+ dc = config[:vim].serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
102
+ baseEntity = dc.vmFolder
103
+ entityArray = folderName.split('/')
104
+ entityArray.each do |entityArrItem|
105
+ if entityArrItem != ''
106
+ baseEntity = baseEntity.childEntity.grep(RbVmomi::VIM::Folder).find { |f| f.name == entityArrItem } or
107
+ abort "no such folder #{folderName} while looking for #{entityArrItem}"
108
+ end
109
+ end
110
+ baseEntity
111
+ end
112
+
113
+ def find_network(networkName)
114
+ dcname = config[:vsphere_dc] || Chef::Config[:knife][:vsphere_dc]
115
+ dc = config[:vim].serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
116
+ baseEntity = dc.network
117
+ baseEntity.find { |f| f.name == networkName } or abort "no such network #{networkName}"
118
+ end
119
+
120
+ def find_pool(poolName)
121
+ dcname = config[:vsphere_dc] || Chef::Config[:knife][:vsphere_dc]
122
+ dc = config[:vim].serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
123
+ baseEntity = dc.hostFolder
124
+ entityArray = poolName.split('/')
125
+ entityArray.each do |entityArrItem|
126
+ if entityArrItem != ''
127
+ if baseEntity.is_a? RbVmomi::VIM::Folder
128
+ baseEntity = baseEntity.childEntity.find { |f| f.name == entityArrItem } or
129
+ abort "no such pool #{poolName} while looking for #{entityArrItem}"
130
+ elsif baseEntity.is_a? RbVmomi::VIM::ClusterComputeResource
131
+ baseEntity = baseEntity.resourcePool.resourcePool.find { |f| f.name == entityArrItem } or
132
+ abort "no such pool #{poolName} while looking for #{entityArrItem}"
133
+ elsif baseEntity.is_a? RbVmomi::VIM::ResourcePool
134
+ baseEntity = baseEntity.resourcePool.find { |f| f.name == entityArrItem } or
135
+ abort "no such pool #{poolName} while looking for #{entityArrItem}"
136
+ else
137
+ abort "Unexpected Object type encountered #{baseEntity.type} while finding resourcePool"
138
+ end
139
+ end
140
+ end
141
+ baseEntity
142
+ end
143
+
144
+ def find_datastore(dsName)
145
+ dcname = config[:vsphere_dc] || Chef::Config[:knife][:vsphere_dc]
146
+ dc = config[:vim].serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
147
+ baseEntity = dc.datastore
148
+ baseEntity.find { |f| f.info.name == dsName } or abort "no such datastore #{dsName}"
149
+ end
150
+
151
+
152
+ def find_all_in_folder(folder, type)
153
+ folder.childEntity.grep(type)
154
+ end
155
+
156
+ def find_in_folder(folder, type, name)
157
+ folder.childEntity.grep(type).find { |o| o.name == name }
158
+ end
159
+
160
+ def fatal_exit(msg)
161
+ ui.fatal(msg)
162
+ exit 1
163
+ end
164
+
165
+ end
166
+ end
167
+ end
@@ -1,29 +1,29 @@
1
- #
2
- # Author:: Ezra Pagel (<ezra@cpan.org>)
3
- # License:: Apache License, Version 2.0
4
- #
5
- require 'chef/knife'
6
- require 'chef/knife/BaseVsphereCommand'
7
-
8
- # Lists all customization specifications in the configured datacenter
9
- class Chef::Knife::VsphereCustomizationList < Chef::Knife::BaseVsphereCommand
10
-
11
- banner "knife vsphere customization list"
12
-
13
- get_common_options
14
-
15
- def run
16
-
17
- $stdout.sync = true
18
-
19
- vim = get_vim_connection
20
-
21
- csm = vim.serviceContent.customizationSpecManager
22
- csm.info.each do |c|
23
- puts "#{ui.color("Customization Name", :cyan)}: #{c.name}"
24
-
25
- end
26
-
27
- end
28
- end
29
-
1
+ #
2
+ # Author:: Ezra Pagel (<ezra@cpan.org>)
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ require 'chef/knife'
6
+ require 'chef/knife/BaseVsphereCommand'
7
+
8
+ # Lists all customization specifications in the configured datacenter
9
+ class Chef::Knife::VsphereCustomizationList < Chef::Knife::BaseVsphereCommand
10
+
11
+ banner "knife vsphere customization list"
12
+
13
+ get_common_options
14
+
15
+ def run
16
+
17
+ $stdout.sync = true
18
+
19
+ vim = get_vim_connection
20
+
21
+ csm = vim.serviceContent.customizationSpecManager
22
+ csm.info.each do |c|
23
+ puts "#{ui.color("Customization Name", :cyan)}: #{c.name}"
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+
@@ -1,35 +1,32 @@
1
- #
2
- # Author:: Ezra Pagel (<ezra@cpan.org>)
3
- # License:: Apache License, Version 2.0
4
- #
5
-
6
- require 'chef/knife'
7
- require 'chef/knife/BaseVsphereCommand'
8
-
9
- # Lists all known VM templates in the configured datacenter
10
- class Chef::Knife::VsphereTemplateList < Chef::Knife::BaseVsphereCommand
11
-
12
- banner "knife vsphere template list"
13
-
14
- get_common_options
15
-
16
- def run
17
-
18
- $stdout.sync = true
19
- $stderr.sync = true
20
-
21
- vim = get_vim_connection
22
-
23
- dcname = config[:vsphere_dc] || Chef::Config[:knife][:vsphere_dc]
24
- dc = vim.serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
25
-
26
- vmFolders = get_folders(dc.vmFolder)
27
-
28
- vms = find_all_in_folders(dc.vmFolder, RbVmomi::VIM::VirtualMachine).
29
- select {|v| !v.config.nil? && v.config.template == true }
30
-
31
- vms.each do |vm|
32
- puts "#{ui.color("Template Name", :cyan)}: #{vm.name}"
33
- end
34
- end
35
- end
1
+ #
2
+ # Author:: Ezra Pagel (<ezra@cpan.org>)
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+
6
+ require 'chef/knife'
7
+ require 'chef/knife/BaseVsphereCommand'
8
+
9
+ # Lists all known VM templates in the configured datacenter
10
+ class Chef::Knife::VsphereTemplateList < Chef::Knife::BaseVsphereCommand
11
+
12
+ banner "knife vsphere template list"
13
+
14
+ get_common_options
15
+
16
+ def run
17
+
18
+ $stdout.sync = true
19
+ $stderr.sync = true
20
+
21
+ vim = get_vim_connection
22
+
23
+ baseFolder = find_folder(config[:folder]);
24
+
25
+ vms = find_all_in_folder(baseFolder, RbVmomi::VIM::VirtualMachine).
26
+ select {|v| !v.config.nil? && v.config.template == true }
27
+
28
+ vms.each do |vm|
29
+ puts "#{ui.color("Template Name", :cyan)}: #{vm.name}"
30
+ end
31
+ end
32
+ end
@@ -1,176 +1,360 @@
1
- #
2
- # Author:: Ezra Pagel (<ezra@cpan.org>)
3
- # License:: Apache License, Version 2.0
4
- #
5
-
6
- require 'chef/knife'
7
- require 'chef/knife/BaseVsphereCommand'
8
- require 'rbvmomi'
9
- require 'netaddr'
10
-
11
- # Clone an existing template into a new VM, optionally applying a customization specification.
12
- #
13
- # usage:
14
- # knife vsphere vm clone NewNode UbuntuTemplate --cspec StaticSpec \
15
- # --cips 192.168.0.99/24,192.168.1.99/24 \
16
- # --chostname NODENAME --cdomain NODEDOMAIN
17
- class Chef::Knife::VsphereVmClone < Chef::Knife::BaseVsphereCommand
18
-
19
- banner "knife vsphere vm clone VMNAME TEMPLATE (options)"
20
-
21
- get_common_options
22
-
23
- option :customization_spec,
24
- :long => "--cspec CUST_SPEC",
25
- :description => "The name of any customization specification to apply"
26
-
27
- option :customization_ips,
28
- :long => "--cips CUST_IPS",
29
- :description => "Comma-delimited list of CIDR IPs for customization"
30
-
31
- option :customization_hostname,
32
- :long => "--chostname CUST_HOSTNAME",
33
- :description => "Unqualified hostname for customization"
34
-
35
- option :customization_domain,
36
- :long => "--cdomain CUST_DOMAIN",
37
- :description => "Domain name for customization"
38
-
39
- option :customization_tz,
40
- :long => "--ctz CUST_TIMEZONE",
41
- :description => "Timezone invalid 'Area/Location' format"
42
-
43
- option :power,
44
- :long => "--start STARTVM",
45
- :description => "Indicates whether to start the VM after a successful clone",
46
- :default => true
47
-
48
-
49
-
50
- def run
51
-
52
- $stdout.sync = true
53
-
54
- vmname = @name_args[0]
55
- if vmname.nil?
56
- show_usage
57
- fatal_exit("You must specify a virtual machine name")
58
- end
59
-
60
- template = @name_args[1]
61
- if template.nil?
62
- show_usage
63
- fatal_exit("You must specify a template name")
64
- end
65
-
66
- vim = get_vim_connection
67
-
68
- dcname = config[:vsphere_dc] || Chef::Config[:knife][:vsphere_dc]
69
- dc = vim.serviceInstance.find_datacenter(dcname) or abort "datacenter not found"
70
-
71
- hosts = find_all_in_folders(dc.hostFolder, RbVmomi::VIM::ComputeResource)
72
- rp = hosts.first.resourcePool
73
-
74
- src_vm = find_in_folders(dc.vmFolder, RbVmomi::VIM::VirtualMachine, template) or
75
- abort "VM/Template not found"
76
-
77
- rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => rp)
78
-
79
-
80
- clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
81
- :powerOn => false,
82
- :template => false)
83
-
84
- if config[:customization_spec]
85
- csi = find_customization(vim, config[:customization_spec]) or
86
- fatal_exit("failed to find customization specification named #{config[:customization_spec]}")
87
-
88
- if csi.info.type != "Linux"
89
- fatal_exit("Only Linux customization specifications are currently supported")
90
- end
91
-
92
- if config[:customization_ips]
93
- csi.spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
94
- end
95
-
96
- use_ident = !config[:customization_hostname].nil? || !config[:customization_domain].nil?
97
-
98
- if use_ident
99
- # TODO - verify that we're deploying a linux spec, at least warn
100
- ident = RbVmomi::VIM.CustomizationLinuxPrep
101
-
102
- if config[:customization_hostname]
103
- ident.hostName = RbVmomi::VIM.CustomizationFixedName
104
- ident.hostName.name = config[:customization_hostname]
105
- else
106
- ident.hostName = RbVmomi::VIM.CustomizationFixedName
107
- ident.hostName.name = config[:customization_domain]
108
- end
109
-
110
- if config[:customization_domain]
111
- ident.domain = config[:customization_domain]
112
- end
113
-
114
- csi.spec.identity = ident
115
- end
116
-
117
- clone_spec.customization = csi.spec
118
-
119
- end
120
-
121
- task = src_vm.CloneVM_Task(:folder => src_vm.parent, :name => vmname, :spec => clone_spec)
122
- puts "Cloning template #{template} to new VM #{vmname}"
123
- task.wait_for_completion
124
- puts "Finished creating virtual machine #{vmname}"
125
-
126
- if config[:power]
127
- vm = find_in_folders(dc.vmFolder, RbVmomi::VIM::VirtualMachine, vmname) or
128
- fatal_exit("VM #{vmname} not found")
129
- vm.PowerOnVM_Task.wait_for_completion
130
- puts "Powered on virtual machine #{vmname}"
131
- end
132
-
133
-
134
- end
135
-
136
-
137
- # Retrieves a CustomizationSpecItem that matches the supplied name
138
- # @param vim [Connection] VI Connection to use
139
- # @param name [String] name of customization
140
- # @return [RbVmomi::VIM::CustomizationSpecItem]
141
- def find_customization(vim, name)
142
- csm = vim.serviceContent.customizationSpecManager
143
- csm.GetCustomizationSpec(:name => name)
144
- end
145
-
146
- # Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
147
- # @param ip [String] Any static IP address to use, otherwise DHCP
148
- # @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
149
- # @return [RbVmomi::VIM::CustomizationIPSettings]
150
- def generate_adapter_map (ip=nil, gw=nil, dns1=nil, dns2=nil, domain=nil)
151
-
152
- settings = RbVmomi::VIM.CustomizationIPSettings
153
-
154
- if ip.nil?
155
- settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator
156
- else
157
- cidr_ip = NetAddr::CIDR.create(ip)
158
- settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cidr_ip.ip)
159
- settings.subnetMask = cidr_ip.netmask_ext
160
-
161
- # TODO - want to confirm gw/ip are in same subnet?
162
- if gw.nil?
163
- settings.gateway = [cidr_ip.network(:Objectify => true).next_ip]
164
- else
165
- gw_cidr = NetAddr::CIDR.create(gw)
166
- settings.gateway = [gw_cidr.ip]
167
- end
168
- end
169
-
170
- adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
171
- adapter_map.adapter = settings
172
- adapter_map
173
-
174
- end
175
-
176
- end
1
+ #
2
+ # Author:: Ezra Pagel (<ezra@cpan.org>)
3
+ # Contributor:: Jesse Campbell (<hikeit@gmail.com>)
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+
7
+ require 'chef/knife'
8
+ require 'chef/knife/BaseVsphereCommand'
9
+ require 'rbvmomi'
10
+ require 'netaddr'
11
+
12
+ # Clone an existing template into a new VM, optionally applying a customization specification.
13
+ #
14
+ # usage:
15
+ # knife vsphere vm clone NewNode UbuntuTemplate --cspec StaticSpec \
16
+ # --cips 192.168.0.99/24,192.168.1.99/24 \
17
+ # --chostname NODENAME --cdomain NODEDOMAIN
18
+ class Chef::Knife::VsphereVmClone < Chef::Knife::BaseVsphereCommand
19
+
20
+ banner "knife vsphere vm clone VMNAME (options)"
21
+
22
+ get_common_options
23
+
24
+ option :dest_folder,
25
+ :long => "--dest-folder FOLDER",
26
+ :description => "The folder into which to put the cloned VM"
27
+
28
+ option :datastore,
29
+ :long => "--datastore STORE",
30
+ :description => "The datastore into which to put the cloned VM"
31
+
32
+ option :resource_pool,
33
+ :long => "--resource-pool POOL",
34
+ :description => "The resource pool into which to put the cloned VM",
35
+ :default => ''
36
+
37
+ option :source_vm,
38
+ :long => "--template TEMPLATE",
39
+ :description => "The source VM / Template to clone from",
40
+ :required => true
41
+
42
+ option :customization_spec,
43
+ :long => "--cspec CUST_SPEC",
44
+ :description => "The name of any customization specification to apply"
45
+
46
+ option :customization_vlan,
47
+ :long => "--cvlan CUST_VLAN",
48
+ :description => "VLAN name for network adapter to join"
49
+
50
+ option :customization_ips,
51
+ :long => "--cips CUST_IPS",
52
+ :description => "Comma-delimited list of CIDR IPs for customization"
53
+
54
+ option :customization_gw,
55
+ :long => "--cgw CUST_GW",
56
+ :description => "CIDR IP of gateway for customization"
57
+
58
+ option :customization_hostname,
59
+ :long => "--chostname CUST_HOSTNAME",
60
+ :description => "Unqualified hostname for customization"
61
+
62
+ option :customization_domain,
63
+ :long => "--cdomain CUST_DOMAIN",
64
+ :description => "Domain name for customization"
65
+
66
+ option :customization_tz,
67
+ :long => "--ctz CUST_TIMEZONE",
68
+ :description => "Timezone invalid 'Area/Location' format"
69
+
70
+ option :customization_cpucount,
71
+ :long => "--ccpu CUST_CPU_COUNT",
72
+ :description => "Number of CPUs"
73
+
74
+ option :customization_memory,
75
+ :long => "--cram CUST_MEMORY_GB",
76
+ :description => "Gigabytes of RAM"
77
+
78
+ option :power,
79
+ :long => "--start STARTVM",
80
+ :description => "Indicates whether to start the VM after a successful clone",
81
+ :default => true
82
+
83
+ option :bootstrap,
84
+ :long => "--bootstrap FALSE",
85
+ :description => "Indicates whether to bootstrap the VM",
86
+ :default => true
87
+
88
+ option :fqdn,
89
+ :long => "--fqdn SERVER_FQDN",
90
+ :description => "Fully qualified hostname for bootstrapping"
91
+
92
+ option :ssh_user,
93
+ :short => "-x USERNAME",
94
+ :long => "--ssh-user USERNAME",
95
+ :description => "The ssh username",
96
+ :default => "root"
97
+
98
+ option :ssh_password,
99
+ :short => "-P PASSWORD",
100
+ :long => "--ssh-password PASSWORD",
101
+ :description => "The ssh password"
102
+
103
+ option :ssh_port,
104
+ :short => "-p PORT",
105
+ :long => "--ssh-port PORT",
106
+ :description => "The ssh port",
107
+ :default => "22",
108
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
109
+
110
+ option :identity_file,
111
+ :short => "-i IDENTITY_FILE",
112
+ :long => "--identity-file IDENTITY_FILE",
113
+ :description => "The SSH identity file used for authentication"
114
+
115
+ option :chef_node_name,
116
+ :short => "-N NAME",
117
+ :long => "--node-name NAME",
118
+ :description => "The Chef node name for your new node"
119
+
120
+ option :prerelease,
121
+ :long => "--prerelease",
122
+ :description => "Install the pre-release chef gems"
123
+
124
+ option :bootstrap_version,
125
+ :long => "--bootstrap-version VERSION",
126
+ :description => "The version of Chef to install",
127
+ :proc => lambda { |v| Chef::Config[:knife][:bootstrap_version] = v }
128
+
129
+ option :bootstrap_proxy,
130
+ :long => "--bootstrap-proxy PROXY_URL",
131
+ :description => "The proxy server for the node being bootstrapped",
132
+ :proc => Proc.new { |p| Chef::Config[:knife][:bootstrap_proxy] = p }
133
+
134
+ option :distro,
135
+ :short => "-d DISTRO",
136
+ :long => "--distro DISTRO",
137
+ :description => "Bootstrap a distro using a template",
138
+ :default => "ubuntu10.04-gems"
139
+
140
+ option :template_file,
141
+ :long => "--template-file TEMPLATE",
142
+ :description => "Full path to location of template to use",
143
+ :default => false
144
+
145
+ option :run_list,
146
+ :short => "-r RUN_LIST",
147
+ :long => "--run-list RUN_LIST",
148
+ :description => "Comma separated list of roles/recipes to apply",
149
+ :proc => lambda { |o| o.split(/[\s,]+/) },
150
+ :default => []
151
+
152
+ option :no_host_key_verify,
153
+ :long => "--no-host-key-verify",
154
+ :description => "Disable host key verification",
155
+ :boolean => true,
156
+ :default => false
157
+
158
+ def run
159
+
160
+ $stdout.sync = true
161
+
162
+ vmname = @name_args[0]
163
+ if vmname.nil?
164
+ show_usage
165
+ fatal_exit("You must specify a virtual machine name")
166
+ end
167
+ config[:fqdn] = vmname unless config[:fqdn]
168
+ config[:chef_node_name] = vmname unless config[:chef_node_name]
169
+
170
+ get_vim_connection
171
+
172
+ src_folder = find_folder(config[:folder])
173
+
174
+ src_vm = find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, config[:source_vm]) or
175
+ abort "VM/Template not found"
176
+
177
+ clone_spec = generate_clone_spec(src_vm.config)
178
+
179
+ dest_folder = find_folder(config[:dest_folder] || config[:folder]);
180
+
181
+ task = src_vm.CloneVM_Task(:folder => dest_folder, :name => vmname, :spec => clone_spec)
182
+ puts "Cloning template #{config[:source_vm]} to new VM #{vmname}"
183
+ task.wait_for_completion
184
+ puts "Finished creating virtual machine #{vmname}"
185
+
186
+ if config[:power] || config[:bootstrap]
187
+ vm = find_in_folder(dest_folder, RbVmomi::VIM::VirtualMachine, vmname) or
188
+ fatal_exit("VM #{vmname} not found")
189
+ vm.PowerOnVM_Task.wait_for_completion
190
+ puts "Powered on virtual machine #{vmname}"
191
+ end
192
+
193
+ if config[:bootstrap]
194
+ print "Waiting for sshd..."
195
+ print "." until tcp_test_ssh(config[:fqdn])
196
+ puts "done"
197
+
198
+ bootstrap_for_node.run
199
+ end
200
+ end
201
+
202
+ # Builds a CloneSpec
203
+ def generate_clone_spec (src_config)
204
+ rspec = RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => find_pool(config[:resource_pool]))
205
+
206
+ if config[:datastore]
207
+ rspec.datastore = find_datastore(config[:datastore])
208
+ end
209
+
210
+ clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec,
211
+ :powerOn => false,
212
+ :template => false)
213
+
214
+ clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new)
215
+
216
+ if config[:customization_cpucount]
217
+ clone_spec.config.numCPUs = config[:customization_cpucount]
218
+ end
219
+
220
+ if config[:customization_memory]
221
+ clone_spec.config.memoryMB = Integer(config[:customization_memory]) * 1024
222
+ end
223
+
224
+ if config[:customization_vlan]
225
+ network = find_network(config[:customization_vlan])
226
+ switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(:switchUuid => network.config.distributedVirtualSwitch.uuid ,:portgroupKey => network.key)
227
+ card = src_config.hardware.device.find { |d| d.deviceInfo.label == "Network adapter 1" } or
228
+ abort "Can't find source network card to customize"
229
+ card.backing.port = switch_port
230
+ dev_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit")
231
+ clone_spec.config.deviceChange.push dev_spec
232
+ end
233
+
234
+ if config[:customization_spec]
235
+ csi = find_customization(config[:customization_spec]) or
236
+ fatal_exit("failed to find customization specification named #{config[:customization_spec]}")
237
+
238
+ if csi.info.type != "Linux"
239
+ fatal_exit("Only Linux customization specifications are currently supported")
240
+ end
241
+
242
+ if config[:customization_ips]
243
+ if config[:customization_gw]
244
+ csi.spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i,config[:customization_gw]) }
245
+ else
246
+ csi.spec.nicSettingMap = config[:customization_ips].split(',').map { |i| generate_adapter_map(i) }
247
+ end
248
+ end
249
+
250
+ use_ident = !config[:customization_hostname].nil? || !config[:customization_domain].nil?
251
+
252
+ if use_ident
253
+ # TODO - verify that we're deploying a linux spec, at least warn
254
+ ident = RbVmomi::VIM.CustomizationLinuxPrep
255
+
256
+ if config[:customization_hostname]
257
+ ident.hostName = RbVmomi::VIM.CustomizationFixedName
258
+ ident.hostName.name = config[:customization_hostname]
259
+ else
260
+ ident.hostName = RbVmomi::VIM.CustomizationFixedName
261
+ ident.hostName.name = config[:customization_domain]
262
+ end
263
+
264
+ if config[:customization_domain]
265
+ ident.domain = config[:customization_domain]
266
+ end
267
+
268
+ csi.spec.identity = ident
269
+ end
270
+
271
+ clone_spec.customization = csi.spec
272
+ end
273
+ clone_spec
274
+ end
275
+
276
+ # Retrieves a CustomizationSpecItem that matches the supplied name
277
+ # @param vim [Connection] VI Connection to use
278
+ # @param name [String] name of customization
279
+ # @return [RbVmomi::VIM::CustomizationSpecItem]
280
+ def find_customization(name)
281
+ csm = config[:vim].serviceContent.customizationSpecManager
282
+ csm.GetCustomizationSpec(:name => name)
283
+ end
284
+
285
+ # Generates a CustomizationAdapterMapping (currently only single IPv4 address) object
286
+ # @param ip [String] Any static IP address to use, otherwise DHCP
287
+ # @param gw [String] If static, the gateway for the interface, otherwise network address + 1 will be used
288
+ # @return [RbVmomi::VIM::CustomizationIPSettings]
289
+ def generate_adapter_map (ip=nil, gw=nil, dns1=nil, dns2=nil, domain=nil)
290
+
291
+ settings = RbVmomi::VIM.CustomizationIPSettings
292
+
293
+ if ip.nil?
294
+ settings.ip = RbVmomi::VIM::CustomizationDhcpIpGenerator
295
+ else
296
+ cidr_ip = NetAddr::CIDR.create(ip)
297
+ settings.ip = RbVmomi::VIM::CustomizationFixedIp(:ipAddress => cidr_ip.ip)
298
+ settings.subnetMask = cidr_ip.netmask_ext
299
+
300
+ # TODO - want to confirm gw/ip are in same subnet?
301
+ # Only set gateway on first IP.
302
+ if config[:customization_ips].split(',').first == ip
303
+ if gw.nil?
304
+ settings.gateway = [cidr_ip.network(:Objectify => true).next_ip]
305
+ else
306
+ gw_cidr = NetAddr::CIDR.create(gw)
307
+ settings.gateway = [gw_cidr.ip]
308
+ end
309
+ end
310
+ end
311
+
312
+ adapter_map = RbVmomi::VIM.CustomizationAdapterMapping
313
+ adapter_map.adapter = settings
314
+ adapter_map
315
+ end
316
+
317
+ def bootstrap_for_node()
318
+ Chef::Knife::Bootstrap.load_deps
319
+ bootstrap = Chef::Knife::Bootstrap.new
320
+ bootstrap.name_args = [config[:fqdn]]
321
+ bootstrap.config[:run_list] = config[:run_list]
322
+ bootstrap.config[:ssh_user] = config[:ssh_user]
323
+ bootstrap.config[:ssh_password] = config[:ssh_password]
324
+ bootstrap.config[:ssh_port] = config[:ssh_port]
325
+ bootstrap.config[:identity_file] = config[:identity_file]
326
+ bootstrap.config[:chef_node_name] = config[:chef_node_name]
327
+ bootstrap.config[:prerelease] = config[:prerelease]
328
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
329
+ bootstrap.config[:distro] = locate_config_value(:distro)
330
+ bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
331
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
332
+ bootstrap.config[:environment] = config[:environment]
333
+ # may be needed for vpc_mode
334
+ bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify]
335
+ bootstrap
336
+ end
337
+
338
+ def tcp_test_ssh(hostname)
339
+ tcp_socket = TCPSocket.new(hostname, 22)
340
+ readable = IO.select([tcp_socket], nil, nil, 5)
341
+ if readable
342
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
343
+ true
344
+ else
345
+ false
346
+ end
347
+ rescue Errno::ETIMEDOUT
348
+ false
349
+ rescue Errno::EPERM
350
+ false
351
+ rescue Errno::ECONNREFUSED
352
+ sleep 2
353
+ false
354
+ rescue Errno::EHOSTUNREACH
355
+ sleep 2
356
+ false
357
+ ensure
358
+ tcp_socket && tcp_socket.close
359
+ end
360
+ end