knife-clodo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/knife-clodo.gemspec +14 -0
- data/lib/chef/knife/bootstrap/debian6apt.erb +51 -0
- data/lib/chef/knife/clodo_base.rb +61 -0
- data/lib/chef/knife/clodo_server_create.rb +216 -0
- data/lib/chef/knife/clodo_server_list.rb +48 -0
- metadata +87 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*~
|
data/knife-clodo.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'knife-clodo'
|
3
|
+
spec.version = '0.1.0'
|
4
|
+
spec.summary = 'Clodo.Ru knife plugin'
|
5
|
+
spec.add_dependency('fog', '>= 0.10.0')
|
6
|
+
spec.description = <<-EOF
|
7
|
+
Knife plugin for Clodo.Ru cloud provider.
|
8
|
+
EOF
|
9
|
+
spec.author = 'Stepan G. Fedorov'
|
10
|
+
spec.email = 'sf@clodo.ru'
|
11
|
+
spec.homepage = 'http://clodo.ru/'
|
12
|
+
spec.files = `git ls-files`.split "\n"
|
13
|
+
spec.require_paths = ["lib"]
|
14
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
bash -c '
|
2
|
+
<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%>
|
3
|
+
|
4
|
+
if [ ! -f /usr/bin/chef-client ]; then
|
5
|
+
echo "chef chef/chef_server_url string <%= @chef_config[:chef_server_url] %>" | debconf-set-selections
|
6
|
+
[ -f /etc/apt/sources.list.d/opscode.list ] || echo "deb http://apt.opscode.com <%= chef_version.to_f == 0.10 ? "squeeze-0.10" : "squeeze" %> main" > /etc/apt/sources.list.d/opscode.list
|
7
|
+
wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %>-O- http://apt.opscode.com/packages@opscode.com.gpg.key | apt-key add -
|
8
|
+
fi
|
9
|
+
|
10
|
+
export DEBIAN_FRONTEND=noninteractive
|
11
|
+
|
12
|
+
apt-get update
|
13
|
+
apt-get install -y ucf chef
|
14
|
+
|
15
|
+
(
|
16
|
+
cat <<'EOP'
|
17
|
+
log_level :info
|
18
|
+
log_location STDOUT
|
19
|
+
ssl_verufy_mode :verify_none
|
20
|
+
chef_server_url "<%= @chef_config[:chef_server_url] %>"
|
21
|
+
EOP
|
22
|
+
) > /etc/chef/client.rb
|
23
|
+
|
24
|
+
(
|
25
|
+
cat <<'EOP'
|
26
|
+
<%= validation_key %>
|
27
|
+
EOP
|
28
|
+
) > /tmp/validation.pem
|
29
|
+
awk NF /tmp/validation.pem > /etc/chef/validation.pem
|
30
|
+
rm /tmp/validation.pem
|
31
|
+
|
32
|
+
<% unless @chef_config[:validation_client_name] == "chef-validator" -%>
|
33
|
+
[ `grep -qx "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" /etc/chef/client.rb` ] || echo "validation_client_name \"<%= @chef_config[:validation_client_name] %>\"" >> /etc/chef/client.rb
|
34
|
+
<% end -%>
|
35
|
+
|
36
|
+
<% if @config[:chef_node_name] %>
|
37
|
+
[ `grep -qx "node_name \"<%= @config[:chef_node_name] %>\"" /etc/chef/client.rb` ] || echo "node_name \"<%= @config[:chef_node_name] %>\"" >> /etc/chef/client.rb
|
38
|
+
<% end -%>
|
39
|
+
|
40
|
+
<% if knife_config[:bootstrap_proxy] %>
|
41
|
+
echo 'http_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb
|
42
|
+
echo 'https_proxy "knife_config[:bootstrap_proxy]"' >> /etc/chef/client.rb
|
43
|
+
<% end -%>
|
44
|
+
|
45
|
+
(
|
46
|
+
cat <<'EOP'
|
47
|
+
<%= { "run_list" => @run_list }.to_json %>
|
48
|
+
EOP
|
49
|
+
) > /etc/chef/first-boot.json
|
50
|
+
|
51
|
+
<%= start_chef %>'
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
class Chef
|
4
|
+
class Knife
|
5
|
+
module ClodoBase
|
6
|
+
|
7
|
+
# I don't know what this means, so just copy it from Rackspace
|
8
|
+
def self.included(includer)
|
9
|
+
includer.class_eval do
|
10
|
+
|
11
|
+
deps do
|
12
|
+
require 'fog'
|
13
|
+
require 'net/ssh/multi'
|
14
|
+
require 'readline'
|
15
|
+
require 'chef/json_compat'
|
16
|
+
end
|
17
|
+
|
18
|
+
option :clodo_api_key,
|
19
|
+
:short => "-K KEY",
|
20
|
+
:long => "--clodo-api-key KEY",
|
21
|
+
:description => "Your clodo.ru API key",
|
22
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:clodo_api_key] = key }
|
23
|
+
|
24
|
+
option :clodo_username,
|
25
|
+
:short => "-A USERNAME",
|
26
|
+
:long => "--clodo-username USERNAME",
|
27
|
+
:description => "Your clodo.ru API username",
|
28
|
+
:proc => Proc.new { |username| Chef::Config[:knife][:clodo_username] = username }
|
29
|
+
|
30
|
+
option :clodo_api_auth_url,
|
31
|
+
:long => "--clodo-api-auth-url URL",
|
32
|
+
:description => "Your clodo.ru API auth url",
|
33
|
+
:default => "api.clodo.ru",
|
34
|
+
:proc => Proc.new { |url| Chef::Config[:knife][:clodo_api_auth_url] = url }
|
35
|
+
end
|
36
|
+
|
37
|
+
def connection
|
38
|
+
@connection ||= Fog::Compute::Clodo.new({
|
39
|
+
:clodo_api_key => Chef::Config[:knife][:clodo_api_key],
|
40
|
+
:clodo_username => (Chef::Config[:knife][:clodo_username] || Chef::Config[:knife][:clodo_api_username]),
|
41
|
+
:clodo_auth_url => Chef::Config[:knife][:clodo_api_auth_url] || config[:clodo_api_auth_url]})
|
42
|
+
end
|
43
|
+
|
44
|
+
def locate_config_value(key)
|
45
|
+
key = key.to_sym
|
46
|
+
Chef::Config[:knife][key] || config[key]
|
47
|
+
end
|
48
|
+
|
49
|
+
def public_dns_name(server)
|
50
|
+
@public_dns_name ||= begin
|
51
|
+
Resolv.getname(server.public_ip_address)
|
52
|
+
rescue
|
53
|
+
"#{server.public_ip_address.gsub('.','-')}.clodo.ru"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'chef/knife/clodo_base'
|
2
|
+
|
3
|
+
class Chef
|
4
|
+
class Knife
|
5
|
+
class ClodoServerCreate < Knife
|
6
|
+
|
7
|
+
include Knife::ClodoBase
|
8
|
+
|
9
|
+
deps do
|
10
|
+
require 'fog'
|
11
|
+
require 'readline'
|
12
|
+
require 'chef/json_compat'
|
13
|
+
require 'chef/knife/bootstrap'
|
14
|
+
Chef::Knife::Bootstrap.load_deps
|
15
|
+
end
|
16
|
+
|
17
|
+
banner "knife clodo.ru server create (options)"
|
18
|
+
|
19
|
+
option :image,
|
20
|
+
:short => "-I IMAGE",
|
21
|
+
:long => "--image IMAGE",
|
22
|
+
:description => "The image of server; default is 541 (Debian 6 amd64 Scale)",
|
23
|
+
:proc => Proc.new { |f| Chef::Config[:knife][:image] = f.to_i },
|
24
|
+
:default => 541
|
25
|
+
|
26
|
+
option :server_name,
|
27
|
+
:short => "-N NAME",
|
28
|
+
:long => "--server-name NAME",
|
29
|
+
:description => "The title for your server"
|
30
|
+
|
31
|
+
option :server_type,
|
32
|
+
:long => "--server-type TYPE",
|
33
|
+
:description => "Type of the server - static or scale (default scale)",
|
34
|
+
:proc => Proc.new {|f| Chef::Config[:knife][:server_type] = f=="static"?"VirtualServer":"ScaleServer"},
|
35
|
+
:default => "ScaleServer"
|
36
|
+
|
37
|
+
option :server_memory,
|
38
|
+
:long => "--server-memory MB",
|
39
|
+
:description => "For static server is an amount of memory in megabytes, for scale server is a low limit in megabytes. (default is 512MB)",
|
40
|
+
:proc => Proc.new {|m| Chef::Config[:knife][:server_memory] = m.to_i},
|
41
|
+
:default => 512
|
42
|
+
|
43
|
+
option :server_memory_max,
|
44
|
+
:long => "--server-memory-max MB",
|
45
|
+
:description => "For static server is ignored, for scale server is a high limit in megabytes. (default is 4096MB)",
|
46
|
+
:proc => Proc.new {|m| Chef::Config[:knife][:server_memory_max] = m.to_i},
|
47
|
+
:default => 4096
|
48
|
+
|
49
|
+
option :server_disk,
|
50
|
+
:long => "--server-disk GB",
|
51
|
+
:description => "Server's disk size in gigabytes. (default 10GB)",
|
52
|
+
:proc => Proc.new {|d| Chef::Config[:knife][:server_disk] = d.to_i},
|
53
|
+
:default => 10
|
54
|
+
|
55
|
+
option :server_support_level,
|
56
|
+
:long => "--server-support-level LEVEL",
|
57
|
+
:description => "Support level for this server. Default is always 1. You can also choose from 2 and 3",
|
58
|
+
:proc => Proc.new {|s| Chef::Config[:knife][:server_support_level] = s.to_i},
|
59
|
+
:default => 1
|
60
|
+
|
61
|
+
option :chef_node_name,
|
62
|
+
:long => "--node-name NAME",
|
63
|
+
:description => "The Chef node name for your new node"
|
64
|
+
|
65
|
+
option :ssh_user,
|
66
|
+
:short => "-x USERNAME",
|
67
|
+
:long => "--ssh-user USERNAME",
|
68
|
+
:description => "The ssh username; default is 'root'",
|
69
|
+
:default => "root"
|
70
|
+
|
71
|
+
option :ssh_password,
|
72
|
+
:short => "-P PASSWORD",
|
73
|
+
:long => "--ssh-password PASSWORD",
|
74
|
+
:description => "The ssh password"
|
75
|
+
|
76
|
+
option :identity_file,
|
77
|
+
:short => "-i IDENTITY_FILE",
|
78
|
+
:long => "--identity-file IDENTITY_FILE",
|
79
|
+
:description => "The SSH identity file used for authentication"
|
80
|
+
|
81
|
+
option :prerelease,
|
82
|
+
:long => "--prerelease",
|
83
|
+
:description => "Install the pre-release chef gems"
|
84
|
+
|
85
|
+
option :bootstrap_version,
|
86
|
+
:long => "--bootstrap-version VERSION",
|
87
|
+
:description => "The version of Chef to install",
|
88
|
+
:proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
|
89
|
+
option :prerelease,
|
90
|
+
:long => "--prerelease",
|
91
|
+
:description => "Install the pre-release chef gems"
|
92
|
+
|
93
|
+
option :bootstrap_version,
|
94
|
+
:long => "--bootstrap-version VERSION",
|
95
|
+
:description => "The version of Chef to install",
|
96
|
+
:proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v }
|
97
|
+
|
98
|
+
option :distro,
|
99
|
+
:short => "-d DISTRO",
|
100
|
+
:long => "--distro DISTRO",
|
101
|
+
:description => "Bootstrap a distro using a template; default is 'debian6apt'",
|
102
|
+
:proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
|
103
|
+
:default => "debian6apt"
|
104
|
+
|
105
|
+
option :template_file,
|
106
|
+
:long => "--template-file TEMPLATE",
|
107
|
+
:description => "Full path to location of template to use",
|
108
|
+
:proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
|
109
|
+
:default => false
|
110
|
+
|
111
|
+
option :run_list,
|
112
|
+
:short => "-r RUN_LIST",
|
113
|
+
:long => "--run-list RUN_LIST",
|
114
|
+
:description => "Comma separated list of roles/recipes to apply",
|
115
|
+
:proc => lambda { |o| o.split(/[\s,]+/) },
|
116
|
+
:default => []
|
117
|
+
|
118
|
+
def tcp_test_ssh(hostname)
|
119
|
+
tcp_socket = TCPSocket.new(hostname, 22)
|
120
|
+
readable = IO.select([tcp_socket], nil, nil, 5)
|
121
|
+
if readable
|
122
|
+
Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
|
123
|
+
yield
|
124
|
+
true
|
125
|
+
else
|
126
|
+
false
|
127
|
+
end
|
128
|
+
rescue Errno::ETIMEDOUT
|
129
|
+
false
|
130
|
+
rescue Errno::EPERM
|
131
|
+
false
|
132
|
+
rescue Errno::ECONNREFUSED
|
133
|
+
sleep 2
|
134
|
+
false
|
135
|
+
rescue Errno::EHOSTUNREACH
|
136
|
+
sleep 2
|
137
|
+
false
|
138
|
+
ensure
|
139
|
+
tcp_socket && tcp_socket.close
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
def run
|
144
|
+
$stdout.sync = true
|
145
|
+
|
146
|
+
unless Chef::Config[:knife][:image]
|
147
|
+
ui.error("You have not provided a valid image value. Please note the short option for this value recently changed from '-i' to '-I'.")
|
148
|
+
exit 1
|
149
|
+
end
|
150
|
+
|
151
|
+
options = {
|
152
|
+
:vps_type => Chef::Config[:knife][:server_type],
|
153
|
+
:vps_memory => Chef::Config[:knife][:server_memory],
|
154
|
+
:vps_memory_max => Chef::Config[:knife][:server_memory_max],
|
155
|
+
:vps_hdd => Chef::Config[:knife][:server_disk],
|
156
|
+
:vps_admin => Chef::Config[:knife][:server_support_level],
|
157
|
+
:vps_os => Chef::Config[:knife][:image]
|
158
|
+
}
|
159
|
+
|
160
|
+
options[:name] = config[:server_name] if config[:server_name]
|
161
|
+
|
162
|
+
server = connection.servers.create(options)
|
163
|
+
|
164
|
+
puts "#{ui.color("ID", :cyan)}: #{server.id}"
|
165
|
+
puts "#{ui.color("Name", :cyan)}: #{server.name}"
|
166
|
+
puts "#{ui.color("Image", :cyan)}: #{server.image}"
|
167
|
+
puts "#{ui.color("IP", :cyan)}: #{server.public_ip_address}"
|
168
|
+
puts "#{ui.color("root password", :red)}: #{server.password}"
|
169
|
+
|
170
|
+
print "\n#{ui.color("Waiting server", :magenta)}"
|
171
|
+
|
172
|
+
# wait for it to be ready to do stuff
|
173
|
+
server.wait_for { print "."; ready? }
|
174
|
+
|
175
|
+
puts("\n")
|
176
|
+
|
177
|
+
puts "#{ui.color("Public DNS Name", :cyan)}: #{public_dns_name(server)}"
|
178
|
+
puts "#{ui.color("Public IP Address", :cyan)}: #{server.public_ip_address}"
|
179
|
+
puts "#{ui.color("Password", :cyan)}: #{server.password}"
|
180
|
+
|
181
|
+
print "\n#{ui.color("Waiting for sshd", :magenta)}"
|
182
|
+
|
183
|
+
print(".") until tcp_test_ssh(server.public_ip_address) { sleep @initial_sleep_delay ||= 10; puts("done") }
|
184
|
+
|
185
|
+
bootstrap_for_node(server).run
|
186
|
+
|
187
|
+
puts "\n"
|
188
|
+
puts "#{ui.color("Instance ID", :cyan)}: #{server.id}"
|
189
|
+
puts "#{ui.color("Name", :cyan)}: #{server.name}"
|
190
|
+
puts "#{ui.color("Image", :cyan)}: #{server.image}"
|
191
|
+
puts "#{ui.color("Public DNS Name", :cyan)}: #{public_dns_name(server)}"
|
192
|
+
puts "#{ui.color("Public IP Address", :cyan)}: #{server.public_ip_address}"
|
193
|
+
puts "#{ui.color("Password", :cyan)}: #{server.password}"
|
194
|
+
puts "#{ui.color("Environment", :cyan)}: #{config[:environment] || '_default'}"
|
195
|
+
puts "#{ui.color("Run List", :cyan)}: #{config[:run_list].join(', ')}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def bootstrap_for_node(server)
|
199
|
+
bootstrap = Chef::Knife::Bootstrap.new
|
200
|
+
bootstrap.name_args = [public_dns_name(server)]
|
201
|
+
bootstrap.config[:run_list] = config[:run_list]
|
202
|
+
bootstrap.config[:ssh_user] = config[:ssh_user] || "root"
|
203
|
+
bootstrap.config[:ssh_password] = server.password
|
204
|
+
bootstrap.config[:identity_file] = config[:identity_file]
|
205
|
+
bootstrap.config[:chef_node_name] = config[:chef_node_name] || server.id
|
206
|
+
bootstrap.config[:prerelease] = config[:prerelease]
|
207
|
+
bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
|
208
|
+
# bootstrap will run as root...sudo (by default) also messes up Ohai on CentOS boxes
|
209
|
+
bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
|
210
|
+
bootstrap.config[:environment] = config[:environment]
|
211
|
+
bootstrap
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'chef/knife/clodo_base'
|
2
|
+
|
3
|
+
class Chef
|
4
|
+
class Knife
|
5
|
+
class ClodoServerList < Knife
|
6
|
+
|
7
|
+
include Knife::ClodoBase
|
8
|
+
|
9
|
+
banner "knife clodo server list (options)"
|
10
|
+
|
11
|
+
def run
|
12
|
+
$stdout.sync = true
|
13
|
+
|
14
|
+
server_list = [
|
15
|
+
ui.color('ID', :bold),
|
16
|
+
ui.color('Name', :bold),
|
17
|
+
ui.color('Public IP', :bold),
|
18
|
+
ui.color('Root pass', :bold),
|
19
|
+
ui.color('VNC', :bold),
|
20
|
+
ui.color('VNC pass', :bold),
|
21
|
+
ui.color('State', :bold)
|
22
|
+
]
|
23
|
+
|
24
|
+
connection.servers.each do |server|
|
25
|
+
server_list << server.id.to_s
|
26
|
+
server_list << server.name
|
27
|
+
server_list << (server.public_ip_address ? server.public_ip_address : "")
|
28
|
+
server_list << server.password
|
29
|
+
server_list << server.vps_vnc
|
30
|
+
server_list << server.vps_vnc_pass
|
31
|
+
server_list << case server.state.downcase
|
32
|
+
when 'is_suspended'
|
33
|
+
ui.color(server.state.downcase, :red)
|
34
|
+
when 'is_disabled', 'is_request'
|
35
|
+
ui.color(server.state.downcase, :cyan)
|
36
|
+
when 'is_install'
|
37
|
+
ui.color(server.state.downcase, :yellow)
|
38
|
+
when 'is_running'
|
39
|
+
ui.color(server.state.downcase, :green)
|
40
|
+
else
|
41
|
+
ui.color(server.state.upcase, :red)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
puts ui.list(server_list, :columns_across, 7)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: knife-clodo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Stepan G. Fedorov
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-24 00:00:00 +04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: fog
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 55
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 10
|
33
|
+
- 0
|
34
|
+
version: 0.10.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: "\tKnife plugin for Clodo.Ru cloud provider.\n"
|
38
|
+
email: sf@clodo.ru
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- knife-clodo.gemspec
|
48
|
+
- lib/chef/knife/bootstrap/debian6apt.erb
|
49
|
+
- lib/chef/knife/clodo_base.rb
|
50
|
+
- lib/chef/knife/clodo_server_create.rb
|
51
|
+
- lib/chef/knife/clodo_server_list.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://clodo.ru/
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 3
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.3.7
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: Clodo.Ru knife plugin
|
86
|
+
test_files: []
|
87
|
+
|