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