knife-proxmox 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
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