knife-proxmox 0.0.12

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.
data/TODO ADDED
@@ -0,0 +1,7 @@
1
+ Description Resource Path Location Type
2
+ FIXME: storage = local debería ser tambien un parametro configurable proxmox_template_list.rb /knife-proxmox/lib/chef line 53 Studio Task
3
+ TODO: Create a good gem using this tutorial http://guides.rubygems.org/make-your-own-gem/#writing-tests
4
+ TODO: All inputs MUST be checked and errors MUST be catched.
5
+ TODO: Testing of everything
6
+ TODO: change ticket.gsub for CGI.escape(str) proxmox_server_create.rb /knife-proxmox/lib/chef line 110 Studio Task
7
+ TODO: parameters for openvz should be in other object proxmox_server_create.rb /knife-proxmox/lib/chef line 55 Studio Task
Binary file
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "knife-proxmox/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "knife-proxmox"
7
+ s.version = Knife::Proxmox::VERSION
8
+ s.has_rdoc = false
9
+ s.authors = ["Jorge Moratilla", "Sergio Galvan"]
10
+ s.email = ["jorge@moratilla.com","sergalma@gmail.com"]
11
+ s.homepage = "http://wiki.opscode.com/display/chef"
12
+ s.summary = "ProxmoxVE Support for Chef's Knife Command"
13
+ s.description = s.summary
14
+ s.extra_rdoc_files = ["README", "LICENSE","TODO","CHANGELOG" ]
15
+
16
+ s.files = Dir['lib/**/*.rb'] + Dir['./*']
17
+ s.add_dependency "chef", ">= 0.10.10"
18
+ s.add_dependency "rest-client", ">=1.6.7"
19
+ s.add_dependency "json", ">=1.5.4"
20
+ s.require_paths = ["lib"]
21
+
22
+ end
@@ -0,0 +1,34 @@
1
+ # This class should be a singleton with all the logic to use proxmox
2
+
3
+ class Connection
4
+
5
+ @site = nil
6
+ @auth_params = nil
7
+ @servers = {}
8
+ @templates = {}
9
+
10
+ def initialize()
11
+ @site = RestClient::Resource.new(Chef::Config[:knife][:pve_cluster_url])
12
+ @auth_params ||= begin
13
+ ticket = nil
14
+ csrf_prevention_token = nil
15
+ @site['access/ticket'].post :username=>Chef::Config[:knife][:pve_user_name],
16
+ :realm=>Chef::Config[:knife][:pve_user_realm],
17
+ :password=>Chef::Config[:knife][:pve_user_password] do |response, request, result, &block|
18
+ if response.code == 200 then
19
+ data = JSON.parse(response.body)
20
+ ticket = data['data']['ticket']
21
+ csrf_prevention_token = data['data']['CSRFPreventionToken']
22
+ if !ticket.nil? then
23
+ token = 'PVEAuthCookie=' + ticket.gsub!(/:/,'%3A').gsub!(/=/,'%3D')
24
+ end
25
+ end
26
+ end
27
+ {:CSRFPreventionToken => csrf_prevention_token, :cookie => token}
28
+ end
29
+
30
+ end
31
+
32
+
33
+
34
+ end
@@ -0,0 +1,251 @@
1
+ require 'chef/knife'
2
+ #TODO: Testing of everything
3
+ #TODO: All inputs MUST be checked and errors MUST be catched.
4
+ class Chef
5
+ class Knife
6
+ module ProxmoxBase
7
+
8
+ def self.included(includer)
9
+ includer.class_eval do
10
+
11
+ deps do
12
+ require 'rubygems'
13
+ require 'rest_client'
14
+ require 'json'
15
+ require 'chef/json_compat'
16
+ require 'cgi'
17
+ require 'chef/log'
18
+ require 'set'
19
+ require 'net/ssh/multi'
20
+ require 'chef/api_client'
21
+ require 'chef/node'
22
+ end
23
+
24
+ # options
25
+ option :pve_cluster_url,
26
+ :short => "-U URL",
27
+ :long => "--pve_cluster_url URL",
28
+ :description => "Your URL to access Proxmox VE server/cluster",
29
+ :proc => Proc.new {|url| Chef::Config[:knife][:pve_cluster_url] = url }
30
+
31
+ option :pve_user_name,
32
+ :short => "-u username",
33
+ :long => "--username username",
34
+ :description => "Your username in Proxmox VE",
35
+ :proc => Proc.new {|username| Chef::Config[:knife][:pve_user_name] = username }
36
+
37
+ option :pve_user_password,
38
+ :short => "-p password",
39
+ :long => "--password password",
40
+ :description => "Your password in Proxmox VE",
41
+ :proc => Proc.new {|password| Chef::Config[:knife][:pve_user_password] = password }
42
+
43
+ option :pve_user_realm,
44
+ :short => "-r realm",
45
+ :long => "--realm realm",
46
+ :description => "Your realm of Authentication in Proxmox VE",
47
+ :proc => Proc.new {|realm| Chef::Config[:knife][:pve_user_realm] = realm }
48
+
49
+ option :pve_node_name,
50
+ :short => "-n node",
51
+ :long => "--node nodename",
52
+ :description => "Proxmox VE server name where you will actuate",
53
+ :proc => Proc.new {|node| Chef::Config[:knife][:pve_node_name] = node }
54
+
55
+ end
56
+ end
57
+
58
+ # Checks that the parameter provided is defined in knife.rb
59
+ def check_global_parameter(value)
60
+ if (Chef::Config[:knife][value].nil? or Chef::Config[:knife][value].empty?) then
61
+ ui.error "knife[:#{value.to_s}] is empty, define a value for it and try again"
62
+ exit 1
63
+ end
64
+ Chef::Log.debug("knife[:#{value}] = " + Chef::Config[:knife][value])
65
+ end
66
+
67
+ def check_config_parameter(value)
68
+ if (config[value].nil? or config[value].empty?) then
69
+ ui.error "--#{value} is empty, define a value for it and try again"
70
+ exit 1
71
+ end
72
+ end
73
+
74
+ # Establishes the connection with proxmox server
75
+ def connection
76
+ # First, let's check we have all info needed to connect to pve
77
+ [:pve_cluster_url, :pve_node_name, :pve_user_name, :pve_user_password, :pve_user_realm].each do |value|
78
+ check_global_parameter(value)
79
+ end
80
+
81
+ @connection ||= RestClient::Resource.new(Chef::Config[:knife][:pve_cluster_url])
82
+ @auth_params ||= begin
83
+ token = nil
84
+ csrf_prevention_token = nil
85
+ @connection['access/ticket'].post :username=>Chef::Config[:knife][:pve_user_name],
86
+ :realm=>Chef::Config[:knife][:pve_user_realm],
87
+ :password=>Chef::Config[:knife][:pve_user_password] do |response, request, result, &block|
88
+ if response.code == 200 then
89
+ data = JSON.parse(response.body)
90
+ ticket = data['data']['ticket']
91
+ csrf_prevention_token = data['data']['CSRFPreventionToken']
92
+ if !ticket.nil? then
93
+ token = 'PVEAuthCookie=' + ticket.gsub!(/:/,'%3A').gsub!(/=/,'%3D')
94
+ end
95
+ end
96
+ end
97
+ {:CSRFPreventionToken => csrf_prevention_token, :cookie => token}
98
+ end
99
+ end
100
+
101
+ # new_vmid: calculates a new vmid from the highest existing vmid
102
+ def new_vmid
103
+ vmid ||= @connection['cluster/resources?type=vm'].get @auth_params do |response, request, result, &block|
104
+ data = JSON.parse(response.body)['data']
105
+ vmids = Set[]
106
+ data.each {|entry|
107
+ vmids.add entry['vmid']
108
+ }
109
+ (vmids.max + 1).to_s
110
+ end
111
+ end
112
+
113
+ # locate_config_value: find a value in arguments or default chef config properties
114
+ def locate_config_value(key)
115
+ key = key.to_sym
116
+ Chef::Config[:knife][key] || config[key]
117
+ end
118
+
119
+ # template_number_to_name: converts the id from the template list to the real name in the storage
120
+ # of the node
121
+ def template_number_to_name(number,storage)
122
+ template_list = []
123
+ #TODO: esta parte hay que sacarla a un modulo comun de acceso a templates
124
+ @connection["nodes/#{Chef::Config[:knife][:pve_node_name]}/storage/#{storage}/content"].get @auth_params do |response, request, result, &block|
125
+ JSON.parse(response.body)['data'].each { |entry|
126
+ if entry['content'] == 'vztmpl' then
127
+ template_list << entry['volid']
128
+ end
129
+ }
130
+ end
131
+ return CGI.escape(template_list[number.to_i])
132
+ end
133
+
134
+ # server_name_to_vmid: Use the name of the server to get the vmid
135
+ def server_name_to_vmid(name)
136
+ @connection['cluster/resources?type=vm'].get @auth_params do |response, request, result, &block|
137
+ data = JSON.parse(response.body)['data']
138
+ data.each {|entry|
139
+ return entry['vmid'] if entry['name'].to_s.match(name)
140
+ }
141
+ end
142
+ end
143
+
144
+ # vmid_to_node: Specify the vmid and get the node in which is
145
+ def vmid_to_node(vmid)
146
+ @connection['cluster/resources?type=vm'].get @auth_params do |response, request, result, &block|
147
+ data = JSON.parse(response.body)['data']
148
+ data.each {|entry|
149
+ return entry['node'] if entry['vmid'].to_s.match(vmid.to_s)
150
+ }
151
+ end
152
+ end
153
+
154
+ def action_response(action,response)
155
+ result = nil
156
+ taskid = nil
157
+ begin
158
+ if (response.code == 200) then
159
+ result = "OK"
160
+ else
161
+ result = "NOK: error code = " + response.code.to_s
162
+ end
163
+ taskid = JSON.parse(response.body)['data']
164
+ waitfor(taskid)
165
+ Chef::Log.debug("Action: #{action}, Result: #{result}\n")
166
+ rescue Exception => msg
167
+ result = "An exception ocurred. Use -VV to show it"
168
+ Chef::Log.debug("Action: #{action}, Result: #{msg}\n")
169
+ end
170
+ ui.msg(result)
171
+ end
172
+
173
+ # waitfor end of the task, need the taskid and the timeout
174
+ def waitfor(taskid, timeout=60)
175
+ taskstatus = nil
176
+ while taskstatus.nil? and timeout>= 0 do
177
+ print "."
178
+ @connection["nodes/#{Chef::Config[:knife][:pve_node_name]}/tasks/#{taskid}/status"].get @auth_params do |response, request, result, &block|
179
+ taskstatus = (JSON.parse(response.body)['data']['status'] == "stopped")?true:nil
180
+ end
181
+ timeout-=1
182
+ sleep(1)
183
+ end
184
+ end
185
+
186
+ def server_start(vmid)
187
+ node = vmid_to_node(vmid)
188
+ ui.msg("Starting VM #{vmid} on node #{node}....")
189
+ @connection["nodes/#{node}/openvz/#{vmid}/status/start"].post "", @auth_params do |response, request, result, &block|
190
+ # take the response and extract the taskid
191
+ action_response("server start",response)
192
+ end
193
+
194
+ end
195
+
196
+ # server_stop: Stops the server
197
+ def server_stop(vmid)
198
+ node = vmid_to_node(vmid)
199
+ ui.msg("Stopping VM #{vmid} on node #{node}...")
200
+ @connection["nodes/#{node}/openvz/#{vmid}/status/stop"].post "", @auth_params do |response, request, result, &block|
201
+ # take the response and extract the taskid
202
+ action_response("server stop",response)
203
+ end
204
+ end
205
+
206
+ def server_create(vmid,vm_definition)
207
+ ui.msg("Creating VM #{vmid}...")
208
+ @connection["nodes/#{Chef::Config[:knife][:pve_node_name]}/openvz"].post "#{vm_definition}", @auth_params do |response, request, result, &block|
209
+ action_response("server create",response)
210
+ end
211
+ end
212
+
213
+ # server_get_address: Returns the IP Address of the machine to chef
214
+ # field is a string, and if it doesn't exist, it will return nil
215
+ def server_get_data(vmid,field)
216
+ node = vmid_to_node(vmid)
217
+ @connection["nodes/#{node}/openvz/#{vmid}/status/current"].get @auth_params do |response, request, result, &block|
218
+ #action_response("server get data",response)
219
+ data = JSON.parse(response.body)['data'][field]
220
+ end
221
+ end
222
+ # server_destroy: Destroys the server
223
+ def server_destroy(vmid)
224
+ node = vmid_to_node(vmid)
225
+ ui.msg("Destroying VM #{vmid} on node #{node}...")
226
+ @connection["nodes/#{node}/openvz/#{vmid}"].delete @auth_params do |response, request, result, &block|
227
+ action_response("server destroy",response)
228
+ end
229
+ end
230
+
231
+ # Extracted from Chef::Knife.delete_object, because it has a
232
+ # confirmation step built in... By specifying the '--purge'
233
+ # flag (and also explicitly confirming the server destruction!)
234
+ # the user is already making their intent known. It is not
235
+ # necessary to make them confirm two more times.
236
+ def destroy_item(klass, name, type_name)
237
+ begin
238
+ object = klass.load(name)
239
+ object.destroy
240
+ ui.warn("Deleted #{type_name} #{name}")
241
+ rescue Net::HTTPServerException
242
+ ui.warn("Could not find a #{type_name} named #{name} to delete!")
243
+ end
244
+ end
245
+
246
+
247
+
248
+
249
+ end # module
250
+ end # class
251
+ end # class
@@ -0,0 +1,234 @@
1
+ require 'chef/knife/proxmox_base'
2
+
3
+ class Chef
4
+ class Knife
5
+ class ProxmoxServerCreate < Knife
6
+
7
+ include Knife::ProxmoxBase
8
+
9
+ deps do
10
+ require 'readline'
11
+ require 'chef/json_compat'
12
+ require 'chef/knife/bootstrap'
13
+ Chef::Knife::Bootstrap.load_deps
14
+ end
15
+
16
+ banner "knife proxmox server create (options)"
17
+
18
+ # TODO: parameters for openvz should be in other object
19
+ option :vm_hostname,
20
+ :short => "-H hostname",
21
+ :long => "--hostname hostname",
22
+ :description => "VM instance hostname"
23
+
24
+ option :vm_cpus,
25
+ :short => "-C CPUs",
26
+ :long => "--cpus number",
27
+ :description => "Number of cpus of the VM instance"
28
+
29
+ option :vm_memory,
30
+ :short => "-M MB",
31
+ :long => "--mem MB",
32
+ :description => "Memory in MB"
33
+
34
+ option :vm_swap,
35
+ :short => "-SW",
36
+ :long => "--swap MB",
37
+ :description => "Memory in MB for swap"
38
+
39
+ option :vm_vmid,
40
+ :short => "-I id",
41
+ :long => "--vmid id",
42
+ :description => "Id for the VM"
43
+
44
+ option :vm_disk,
45
+ :short => "-D disk",
46
+ :long => "--disk GB",
47
+ :description => "Disk space in GB"
48
+
49
+ option :vm_storage,
50
+ :short => "-ST name",
51
+ :long => "--storage name",
52
+ :description => "Name of the storage where to reserve space"
53
+
54
+ option :vm_password,
55
+ :short => "-P password",
56
+ :long => "--vm_pass password",
57
+ :description => "root password for VM (openvz only)",
58
+ :default => "proxmox"
59
+
60
+ option :vm_netif,
61
+ :short => "-N netif",
62
+ :long => "--netif netif_specification",
63
+ :description => "description of the network interface (experimental)"
64
+
65
+ option :vm_template,
66
+ :short => "-T number",
67
+ :long => "--template number",
68
+ :description => "id of the template"
69
+
70
+ option :vm_ipaddress,
71
+ :short => "-ip ipaddress",
72
+ :long => "--ipaddress IP Address",
73
+ :description => "force guest to use venet interface with this ip address"
74
+
75
+ option :bootstrap,
76
+ :long => "--[no-]bootstrap",
77
+ :description => "Bootstrap the server, enable by default",
78
+ :boolean => true,
79
+ :default => true
80
+
81
+ option :bootstrap_version,
82
+ :long => "--bootstrap-version VERSION",
83
+ :description => "The version of Chef to install",
84
+ :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
85
+
86
+ option :distro,
87
+ :short => "-d DISTRO",
88
+ :long => "--distro DISTRO",
89
+ :description => "Bootstrap a distro using a template; default is 'chef-full'",
90
+ :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
91
+ :default => "chef-full"
92
+
93
+ option :template_file,
94
+ :long => "--template-file TEMPLATE",
95
+ :description => "Full path to location of template to use",
96
+ :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
97
+ :default => false
98
+
99
+ option :run_list,
100
+ :short => "-r RUN_LIST",
101
+ :long => "--run-list RUN_LIST",
102
+ :description => "Comma separated list of roles/recipes to apply",
103
+ :proc => lambda { |o| o.split(/[\s,]+/) },
104
+ :default => []
105
+
106
+ option :first_boot_attributes,
107
+ :short => "-j JSON_ATTRIBS",
108
+ :long => "--json-attributes",
109
+ :description => "A JSON string to be added to the first run of chef-client",
110
+ :proc => lambda { |o| JSON.parse(o) },
111
+ :default => {}
112
+
113
+ option :identity_file,
114
+ :short => "-i IDENTITY_FILE",
115
+ :long => "--identity-file IDENTITY_FILE",
116
+ :description => "The SSH identity file used for authentication"
117
+
118
+ option :host_key_verify,
119
+ :long => "--[no-]host-key-verify",
120
+ :description => "Verify host key, enabled by default",
121
+ :boolean => true,
122
+ :default => true
123
+
124
+ option :environment,
125
+ :short=> "-e environment",
126
+ :long => "--environment environment",
127
+ :description => "Chef environment",
128
+ :proc => Proc.new {|env| Chef::Config[:knife][:environment] = env },
129
+ :default => '_default'
130
+
131
+ def run
132
+ # Needed
133
+ connection
134
+
135
+ vm_id = config[:vm_vmid] || new_vmid
136
+ vm_hostname = config[:vm_hostname] || 'proxmox'
137
+ vm_storage = config[:vm_storage] || 'local'
138
+ vm_password = config[:vm_password] || 'pve123'
139
+ vm_cpus = config[:vm_cpus] || 1
140
+ vm_memory = config[:vm_memory] || 512
141
+ vm_disk = config[:vm_disk] || 4
142
+ vm_swap = config[:vm_swap] || 512
143
+ vm_ipaddress= config[:vm_ipaddress]|| nil
144
+ vm_netif = config[:vm_netif] || 'ifname%3Deth0%2Cbridge%3Dvmbr0'
145
+ vm_template = template_number_to_name(config[:vm_template],vm_storage) || 'local%3Avztmpl%2Fubuntu-11.10-x86_64-jorge2-.tar.gz'
146
+
147
+ vm_definition = "vmid=#{vm_id}&hostname=#{vm_hostname}&storage=#{vm_storage}&password=#{vm_password}&ostemplate=#{vm_template}&memory=#{vm_memory}&swap=#{vm_swap}&disk=#{vm_disk}&cpus=#{vm_cpus}"
148
+
149
+ # Add ip_address parameter to vm_definition if it's provided by CLI
150
+ if (config[:vm_ipaddress]) then
151
+ vm_definition += "&ip_address=" + vm_ipaddress
152
+ elsif (config[:vm_netif] || vm_netif) then
153
+ vm_definition += "&netif=" + vm_netif
154
+ end
155
+
156
+ Chef::Log.debug(vm_definition)
157
+
158
+ server_create(vm_id,vm_definition)
159
+ ui.msg("Preparing the server to start")
160
+ sleep(5)
161
+ server_start(vm_id)
162
+ sleep(5)
163
+
164
+ # which IP address to bootstrap
165
+ bootstrap_ip_address = server_get_data(vm_id,'ip')
166
+ ui.msg("New Server #{vm_id} has IP Address: #{server_get_data(vm_id,'ip')}")
167
+
168
+ if bootstrap_ip_address.nil?
169
+ ui.error("No IP address available for bootstrapping.")
170
+ exit 1
171
+ end
172
+
173
+ print(".") until tcp_test_ssh(bootstrap_ip_address) {
174
+ sleep @initial_sleep_delay ||= 10
175
+ puts("done")
176
+ }
177
+
178
+ # bootstrapping the node
179
+ if config[:bootstrap]
180
+ bootstrap_for_node(bootstrap_ip_address).run
181
+ else
182
+ ui.msg("Skipping bootstrap of the server because --no-bootstrap used as argument.")
183
+ end
184
+
185
+ end
186
+
187
+ def tcp_test_ssh(hostname)
188
+ tcp_socket = TCPSocket.new(hostname, 22)
189
+ readable = IO.select([tcp_socket], nil, nil, 5)
190
+ if readable
191
+ Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
192
+ yield
193
+ true
194
+ else
195
+ false
196
+ end
197
+ rescue Errno::ETIMEDOUT
198
+ false
199
+ rescue Errno::EPERM
200
+ false
201
+ rescue Errno::ECONNREFUSED
202
+ sleep 2
203
+ false
204
+ rescue Errno::EHOSTUNREACH
205
+ sleep 2
206
+ false
207
+ ensure
208
+ tcp_socket && tcp_socket.close
209
+ end
210
+
211
+ def bootstrap_for_node(bootstrap_ip_address)
212
+ bootstrap = Chef::Knife::Bootstrap.new
213
+ bootstrap.name_args = [bootstrap_ip_address]
214
+ bootstrap.config[:run_list] = config[:run_list]
215
+ bootstrap.config[:environment] = locate_config_value(:environment)
216
+ # bootstrap.config[:first_boot_attributes] = config[:first_boot_attributes]
217
+ bootstrap.config[:ssh_user] = "root"
218
+ bootstrap.config[:ssh_password] = config[:vm_password]
219
+ # bootstrap.config[:identity_file] = config[:identity_file]
220
+ # bootstrap.config[:host_key_verify] = config[:host_key_verify]
221
+ bootstrap.config[:chef_node_name] = config[:vm_hostname]
222
+ # bootstrap.config[:prerelease] = config[:prerelease]
223
+ bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
224
+ bootstrap.config[:distro] = locate_config_value(:distro)
225
+ # bootstrap will run as root...sudo (by default) also messes up Ohai on CentOS boxes
226
+ # bootstrap.config[:use_sudo] = false
227
+ bootstrap.config[:template_file] = locate_config_value(:template_file)
228
+
229
+ pp bootstrap.config
230
+ bootstrap
231
+ end
232
+ end
233
+ end
234
+ end