knife-vsphere 0.1.5 → 0.1.6

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