kytoon 1.0.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/.document +5 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +29 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +81 -0
- data/Rakefile +35 -0
- data/VERSION +1 -0
- data/config/server_group_vpc.json +14 -0
- data/config/server_group_xen.json +24 -0
- data/lib/kytoon.rb +8 -0
- data/lib/kytoon/providers/cloud_servers_vpc.rb +6 -0
- data/lib/kytoon/providers/cloud_servers_vpc/client.rb +197 -0
- data/lib/kytoon/providers/cloud_servers_vpc/connection.rb +148 -0
- data/lib/kytoon/providers/cloud_servers_vpc/server.rb +121 -0
- data/lib/kytoon/providers/cloud_servers_vpc/server_group.rb +401 -0
- data/lib/kytoon/providers/cloud_servers_vpc/ssh_public_key.rb +29 -0
- data/lib/kytoon/providers/cloud_servers_vpc/vpn_network_interface.rb +33 -0
- data/lib/kytoon/providers/xenserver.rb +1 -0
- data/lib/kytoon/providers/xenserver/server_group.rb +371 -0
- data/lib/kytoon/server_group.rb +46 -0
- data/lib/kytoon/ssh_util.rb +23 -0
- data/lib/kytoon/util.rb +118 -0
- data/lib/kytoon/version.rb +8 -0
- data/lib/kytoon/vpn/vpn_connection.rb +46 -0
- data/lib/kytoon/vpn/vpn_network_manager.rb +237 -0
- data/lib/kytoon/vpn/vpn_openvpn.rb +112 -0
- data/lib/kytoon/xml_util.rb +15 -0
- data/rake/kytoon.rake +115 -0
- data/test/client_test.rb +111 -0
- data/test/helper.rb +18 -0
- data/test/server_group_test.rb +253 -0
- data/test/server_test.rb +69 -0
- data/test/ssh_util_test.rb +22 -0
- data/test/test_helper.rb +194 -0
- data/test/test_kytoon.rb +7 -0
- data/test/util_test.rb +23 -0
- data/test/vpn_network_manager_test.rb +40 -0
- metadata +247 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Kytoon
|
2
|
+
|
3
|
+
module SshUtil
|
4
|
+
|
5
|
+
def self.remove_known_hosts_ip(ip, known_hosts_file=File.join(ENV['HOME'], ".ssh", "known_hosts"))
|
6
|
+
|
7
|
+
return if ip.nil? or ip.empty?
|
8
|
+
return if not FileTest.exist?(known_hosts_file)
|
9
|
+
|
10
|
+
existing=IO.read(known_hosts_file)
|
11
|
+
File.open(known_hosts_file, 'w') do |file|
|
12
|
+
existing.each_line do |line|
|
13
|
+
if not line =~ Regexp.new("^#{ip}.*$") then
|
14
|
+
file.write(line)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/kytoon/util.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'socket'
|
3
|
+
require 'kytoon/server_group'
|
4
|
+
|
5
|
+
module Kytoon
|
6
|
+
|
7
|
+
module Util
|
8
|
+
|
9
|
+
SSH_OPTS="-o StrictHostKeyChecking=no"
|
10
|
+
|
11
|
+
@@configs=nil
|
12
|
+
|
13
|
+
def self.hostname
|
14
|
+
Socket.gethostname
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.load_configs
|
18
|
+
|
19
|
+
return @@configs if not @@configs.nil?
|
20
|
+
|
21
|
+
config_file=ENV['KYTOON_CONFIG_FILE']
|
22
|
+
if config_file.nil? then
|
23
|
+
|
24
|
+
config_file=ENV['HOME']+File::SEPARATOR+".kytoon.conf"
|
25
|
+
if not File.exists?(config_file) then
|
26
|
+
config_file="/etc/kytoon.conf"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
if File.exists?(config_file) then
|
32
|
+
configs=YAML.load_file(config_file)
|
33
|
+
raise_if_nil_or_empty(configs, "cloud_servers_vpc_url")
|
34
|
+
raise_if_nil_or_empty(configs, "cloud_servers_vpc_username")
|
35
|
+
raise_if_nil_or_empty(configs, "cloud_servers_vpc_password")
|
36
|
+
@@configs=configs
|
37
|
+
else
|
38
|
+
raise "Failed to load kytoon config file. Please configure /etc/kytoon.conf or create a .kytoon.conf config file in your HOME directory."
|
39
|
+
end
|
40
|
+
|
41
|
+
@@configs
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.load_public_key
|
46
|
+
|
47
|
+
ssh_dir=ENV['HOME']+File::SEPARATOR+".ssh"+File::SEPARATOR
|
48
|
+
if File.exists?(ssh_dir+"id_rsa.pub")
|
49
|
+
pubkey=IO.read(ssh_dir+"id_rsa.pub")
|
50
|
+
elsif File.exists?(ssh_dir+"id_dsa.pub")
|
51
|
+
pubkey=IO.read(ssh_dir+"id_dsa.pub")
|
52
|
+
else
|
53
|
+
raise "Failed to load SSH key. Please create a SSH public key pair in your HOME directory."
|
54
|
+
end
|
55
|
+
|
56
|
+
pubkey.chomp
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.raise_if_nil_or_empty(options, key)
|
61
|
+
if not options or options[key].nil? or options[key].empty? then
|
62
|
+
raise "Please specify a valid #{key.to_s} parameter."
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.remote_exec(script_text, gateway_ip)
|
67
|
+
if gateway_ip.nil?
|
68
|
+
sg=ServerGroup.get
|
69
|
+
gateway_ip=sg.gateway_ip
|
70
|
+
end
|
71
|
+
|
72
|
+
out=%x{
|
73
|
+
ssh #{SSH_OPTS} root@#{gateway_ip} bash <<-"REMOTE_EXEC_EOF"
|
74
|
+
#{script_text}
|
75
|
+
REMOTE_EXEC_EOF
|
76
|
+
}
|
77
|
+
retval=$?
|
78
|
+
if block_given? then
|
79
|
+
yield retval.success?, out
|
80
|
+
else
|
81
|
+
return [retval.success?, out]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.remote_multi_exec(hosts, script_text, gateway_ip)
|
86
|
+
|
87
|
+
if gateway_ip.nil?
|
88
|
+
sg=ServerGroup.get
|
89
|
+
gateway_ip=sg.gateway_ip
|
90
|
+
end
|
91
|
+
|
92
|
+
results = {}
|
93
|
+
threads = []
|
94
|
+
|
95
|
+
hosts.each do |host|
|
96
|
+
t = Thread.new do
|
97
|
+
out=%x{
|
98
|
+
ssh #{SSH_OPTS} root@#{gateway_ip} bash <<-"REMOTE_EXEC_EOF"
|
99
|
+
ssh #{host} bash <<-"EOF_HOST"
|
100
|
+
#{script_text}
|
101
|
+
EOF_HOST
|
102
|
+
REMOTE_EXEC_EOF
|
103
|
+
}
|
104
|
+
retval=$?
|
105
|
+
results.store host, [retval.success?, out]
|
106
|
+
end
|
107
|
+
threads << t
|
108
|
+
end
|
109
|
+
|
110
|
+
threads.each {|t| t.join}
|
111
|
+
|
112
|
+
return results
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
module Kytoon
|
3
|
+
module Vpn
|
4
|
+
class VpnConnection
|
5
|
+
|
6
|
+
CERT_DIR=File.join(ENV['HOME'], '.pki', 'openvpn')
|
7
|
+
|
8
|
+
def initialize(group, client = nil)
|
9
|
+
@group = group
|
10
|
+
@client = client
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_certs
|
14
|
+
@ca_cert=get_cfile('ca.crt')
|
15
|
+
@client_cert=get_cfile('client.crt')
|
16
|
+
@client_key=get_cfile('client.key')
|
17
|
+
|
18
|
+
vpn_interface = @client.vpn_network_interfaces[0]
|
19
|
+
|
20
|
+
FileUtils.mkdir_p(get_cfile)
|
21
|
+
File::chmod(0700, File.join(ENV['HOME'], '.pki'))
|
22
|
+
File::chmod(0700, CERT_DIR)
|
23
|
+
|
24
|
+
File.open(@ca_cert, 'w') { |f| f.write(vpn_interface.ca_cert) }
|
25
|
+
File.open(@client_cert, 'w') { |f| f.write(vpn_interface.client_cert) }
|
26
|
+
File.open(@client_key, 'w') do |f|
|
27
|
+
f.write(vpn_interface.client_key)
|
28
|
+
f.chmod(0600)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_certs
|
33
|
+
FileUtils.rm_rf(get_cfile)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_cfile(file = nil)
|
37
|
+
if file
|
38
|
+
File.join(CERT_DIR, @group.id.to_s, file)
|
39
|
+
else
|
40
|
+
File.join(CERT_DIR, @group.id.to_s)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'builder'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'rexml/xpath'
|
5
|
+
require 'uuidtools'
|
6
|
+
require 'ipaddr'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'tempfile'
|
9
|
+
|
10
|
+
module Kytoon
|
11
|
+
module Vpn
|
12
|
+
|
13
|
+
class VpnNetworkManager < VpnConnection
|
14
|
+
|
15
|
+
def initialize(group, client = nil)
|
16
|
+
super(group, client)
|
17
|
+
end
|
18
|
+
|
19
|
+
def connect
|
20
|
+
create_certs
|
21
|
+
configure_gconf
|
22
|
+
puts %x{#{sudo_display} nmcli con up id "VPC Group: #{@group.id}"}
|
23
|
+
end
|
24
|
+
|
25
|
+
def disconnect
|
26
|
+
puts %x{#{sudo_display} nmcli con down id "VPC Group: #{@group.id}"}
|
27
|
+
end
|
28
|
+
|
29
|
+
def connected?
|
30
|
+
return system("#{sudo_display} nmcli con status | grep -c 'VPC Group: #{@group.id}' &> /dev/null")
|
31
|
+
end
|
32
|
+
|
33
|
+
def clean
|
34
|
+
unset_gconf_config
|
35
|
+
delete_certs
|
36
|
+
end
|
37
|
+
|
38
|
+
def configure_gconf
|
39
|
+
|
40
|
+
xml = Builder::XmlMarkup.new
|
41
|
+
xml.gconfentryfile do |file|
|
42
|
+
file.entrylist({ "base" => "/system/networking/connections/vpc_#{@group.id}"}) do |entrylist|
|
43
|
+
|
44
|
+
entrylist.entry do |entry|
|
45
|
+
entry.key("connection/autoconnect")
|
46
|
+
entry.value do |value|
|
47
|
+
value.bool("false")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
entrylist.entry do |entry|
|
51
|
+
entry.key("connection/id")
|
52
|
+
entry.value do |value|
|
53
|
+
value.string("VPC Group: #{@group.id}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
entrylist.entry do |entry|
|
57
|
+
entry.key("connection/name")
|
58
|
+
entry.value do |value|
|
59
|
+
value.string("connection")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
entrylist.entry do |entry|
|
63
|
+
entry.key("connection/timestamp")
|
64
|
+
entry.value do |value|
|
65
|
+
value.string(Time.now.to_i.to_s)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
entrylist.entry do |entry|
|
69
|
+
entry.key("connection/type")
|
70
|
+
entry.value do |value|
|
71
|
+
value.string("vpn")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
entrylist.entry do |entry|
|
75
|
+
entry.key("connection/uuid")
|
76
|
+
entry.value do |value|
|
77
|
+
value.string(UUIDTools::UUID.random_create)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
entrylist.entry do |entry|
|
81
|
+
entry.key("ipv4/addresses")
|
82
|
+
entry.value do |value|
|
83
|
+
value.list("type" => "int") do |list|
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
entrylist.entry do |entry|
|
88
|
+
entry.key("ipv4/dns")
|
89
|
+
entry.value do |value|
|
90
|
+
value.list("type" => "int") do |list|
|
91
|
+
ip=IPAddr.new(@group.vpn_network.chomp("0")+"1")
|
92
|
+
list.value do |lv|
|
93
|
+
lv.int(ip_to_integer(ip.to_s))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
entrylist.entry do |entry|
|
99
|
+
entry.key("ipv4/dns-search")
|
100
|
+
entry.value do |value|
|
101
|
+
value.list("type" => "string") do |list|
|
102
|
+
list.value do |lv|
|
103
|
+
lv.string(@group.domain_name)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
entrylist.entry do |entry|
|
109
|
+
entry.key("ipv4/ignore-auto-dns")
|
110
|
+
entry.value do |value|
|
111
|
+
value.bool("true")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
entrylist.entry do |entry|
|
115
|
+
entry.key("ipv4/method")
|
116
|
+
entry.value do |value|
|
117
|
+
value.string("auto")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
entrylist.entry do |entry|
|
121
|
+
entry.key("ipv4/name")
|
122
|
+
entry.value do |value|
|
123
|
+
value.string("ipv4")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
entrylist.entry do |entry|
|
127
|
+
entry.key("ipv4/never-default")
|
128
|
+
entry.value do |value|
|
129
|
+
value.bool("true")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
entrylist.entry do |entry|
|
133
|
+
entry.key("ipv4/routes")
|
134
|
+
entry.value do |value|
|
135
|
+
value.list("type" => "int") do |list|
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
entrylist.entry do |entry|
|
140
|
+
entry.key("vpn/ca")
|
141
|
+
entry.value do |value|
|
142
|
+
value.string(@ca_cert)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
entrylist.entry do |entry|
|
146
|
+
entry.key("vpn/cert")
|
147
|
+
entry.value do |value|
|
148
|
+
value.string(@client_cert)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
entrylist.entry do |entry|
|
152
|
+
entry.key("vpn/comp-lzo")
|
153
|
+
entry.value do |value|
|
154
|
+
value.string("yes")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
entrylist.entry do |entry|
|
158
|
+
entry.key("vpn/connection-type")
|
159
|
+
entry.value do |value|
|
160
|
+
value.string("tls")
|
161
|
+
end
|
162
|
+
end
|
163
|
+
entrylist.entry do |entry|
|
164
|
+
entry.key("vpn/key")
|
165
|
+
entry.value do |value|
|
166
|
+
value.string(@client_key)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
if @group.vpn_proto == "tcp"
|
170
|
+
entrylist.entry do |entry|
|
171
|
+
entry.key("vpn/proto-tcp")
|
172
|
+
entry.value do |value|
|
173
|
+
value.string("yes")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
else
|
177
|
+
entrylist.entry do |entry|
|
178
|
+
entry.key("vpn/proto-udp")
|
179
|
+
entry.value do |value|
|
180
|
+
value.string("yes")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
if @group.vpn_device == "tap"
|
185
|
+
entrylist.entry do |entry|
|
186
|
+
entry.key("vpn/tap-dev")
|
187
|
+
entry.value do |value|
|
188
|
+
value.string("yes")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
entrylist.entry do |entry|
|
193
|
+
entry.key("vpn/remote")
|
194
|
+
entry.value do |value|
|
195
|
+
value.string(@group.gateway_ip)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
entrylist.entry do |entry|
|
199
|
+
entry.key("vpn/service-type")
|
200
|
+
entry.value do |value|
|
201
|
+
value.string("org.freedesktop.NetworkManager.openvpn")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
Tempfile.open('w') do |f|
|
209
|
+
f.write(xml.target!)
|
210
|
+
f.flush
|
211
|
+
puts %x{gconftool-2 --load #{f.path}}
|
212
|
+
end
|
213
|
+
|
214
|
+
return true
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
def unset_gconf_config
|
219
|
+
puts %x{gconftool-2 --recursive-unset /system/networking/connections/vpc_#{@group.id}}
|
220
|
+
end
|
221
|
+
|
222
|
+
def ip_to_integer(ip_string)
|
223
|
+
return 0 if ip_string.nil?
|
224
|
+
ip_arr=ip_string.split(".").collect{ |s| s.to_i }
|
225
|
+
return ip_arr[0] + ip_arr[1]*2**8 + ip_arr[2]*2**16 + ip_arr[3]*2**24
|
226
|
+
end
|
227
|
+
|
228
|
+
def sudo_display
|
229
|
+
if ENV['DISPLAY'].nil? or ENV['DISPLAY'] != ":0.0" then
|
230
|
+
"sudo"
|
231
|
+
else
|
232
|
+
""
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Kytoon
|
2
|
+
module Vpn
|
3
|
+
class VpnOpenVpn < VpnConnection
|
4
|
+
|
5
|
+
def initialize(group, client = nil)
|
6
|
+
super(group, client)
|
7
|
+
end
|
8
|
+
|
9
|
+
def connect
|
10
|
+
create_certs
|
11
|
+
|
12
|
+
@up_script=get_cfile('up.bash')
|
13
|
+
File.open(@up_script, 'w') do |f|
|
14
|
+
f << <<EOF_UP
|
15
|
+
#!/bin/bash
|
16
|
+
|
17
|
+
# setup routes
|
18
|
+
/sbin/route add #{@group.vpn_network.chomp("0")+"1"} dev \$dev
|
19
|
+
/sbin/route add -net #{@group.vpn_network} netmask 255.255.128.0 gw #{@group.vpn_network.chomp("0")+"1"}
|
20
|
+
|
21
|
+
mv /etc/resolv.conf /etc/resolv.conf.bak
|
22
|
+
egrep ^search /etc/resolv.conf.bak | sed -e 's/search /search #{@group.domain_name} /' > /etc/resolv.conf
|
23
|
+
echo 'nameserver #{@group.vpn_network.chomp("0")+"1"}' >> /etc/resolv.conf
|
24
|
+
grep ^nameserver /etc/resolv.conf.bak >> /etc/resolv.conf
|
25
|
+
EOF_UP
|
26
|
+
f.chmod(0700)
|
27
|
+
end
|
28
|
+
@down_script=get_cfile('down.bash')
|
29
|
+
File.open(@down_script, 'w') do |f|
|
30
|
+
f << <<EOF_DOWN
|
31
|
+
#!/bin/bash
|
32
|
+
mv /etc/resolv.conf.bak /etc/resolv.conf
|
33
|
+
EOF_DOWN
|
34
|
+
f.chmod(0700)
|
35
|
+
end
|
36
|
+
|
37
|
+
@config_file=get_cfile('config')
|
38
|
+
File.open(@config_file, 'w') do |f|
|
39
|
+
f << <<EOF_CONFIG
|
40
|
+
client
|
41
|
+
dev #{@group.vpn_device}
|
42
|
+
proto #{@group.vpn_proto}
|
43
|
+
|
44
|
+
#Change my.publicdomain.com to your public domain or IP address
|
45
|
+
remote #{@group.gateway_ip} 1194
|
46
|
+
|
47
|
+
resolv-retry infinite
|
48
|
+
nobind
|
49
|
+
persist-key
|
50
|
+
persist-tun
|
51
|
+
|
52
|
+
script-security 2
|
53
|
+
|
54
|
+
ca #{@ca_cert}
|
55
|
+
cert #{@client_cert}
|
56
|
+
key #{@client_key}
|
57
|
+
|
58
|
+
ns-cert-type server
|
59
|
+
|
60
|
+
route-nopull
|
61
|
+
|
62
|
+
comp-lzo
|
63
|
+
|
64
|
+
verb 3
|
65
|
+
up #{@up_script}
|
66
|
+
down #{@down_script}
|
67
|
+
EOF_CONFIG
|
68
|
+
f.chmod(0600)
|
69
|
+
end
|
70
|
+
|
71
|
+
disconnect if File.exist?(get_cfile('openvpn.pid'))
|
72
|
+
out=%x{sudo openvpn --config #{@config_file} --writepid #{get_cfile('openvpn.pid')} --daemon}
|
73
|
+
retval=$?
|
74
|
+
if retval.success? then
|
75
|
+
poll_vpn_interface
|
76
|
+
puts "OK."
|
77
|
+
else
|
78
|
+
raise "Failed to create VPN connection: #{out}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def disconnect
|
83
|
+
raise "Not running? No pid file found!" unless File.exist?(get_cfile('openvpn.pid'))
|
84
|
+
pid = File.read(get_cfile('openvpn.pid')).chomp
|
85
|
+
system("sudo kill -TERM #{pid}")
|
86
|
+
File.delete(get_cfile('openvpn.pid'))
|
87
|
+
end
|
88
|
+
|
89
|
+
def connected?
|
90
|
+
system("/sbin/route -n | grep #{@group.vpn_network.chomp("0")+"1"} &> /dev/null")
|
91
|
+
end
|
92
|
+
|
93
|
+
def clean
|
94
|
+
delete_certs
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def poll_vpn_interface
|
99
|
+
interface_name=@group.vpn_device+"0"
|
100
|
+
1.upto(30) do |i|
|
101
|
+
break if system("/sbin/ifconfig #{interface_name} > /dev/null 2>&1")
|
102
|
+
if i == 30 then
|
103
|
+
disconnect
|
104
|
+
raise "Failed to connect to VPN."
|
105
|
+
end
|
106
|
+
sleep 0.5
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|