bcome 0.0.3 → 0.0.4
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.
- checksums.yaml +4 -4
- data/documentation/usage.md +21 -21
- data/lib/bcome.rb +4 -1
- data/lib/bcome/version.rb +1 -1
- data/lib/become_object.rb +9 -0
- data/lib/boot.rb +8 -0
- data/lib/helpers/command_helper.rb +2 -2
- data/lib/helpers/environment_ssh.rb +12 -2
- data/lib/helpers/instance_command.rb +21 -6
- data/lib/helpers/selections.rb +7 -8
- data/lib/object.rb +17 -3
- data/lib/patches/string.rb +4 -0
- data/lib/scp.rb +17 -6
- data/lib/stack/environment.rb +7 -2
- data/lib/stack/instance.rb +17 -4
- data/lib/workspace_context.rb +7 -4
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8b43a40c66431c8282e710e49721519ae9cdd22
|
4
|
+
data.tar.gz: 6aab609acf080f4686b1ebceb016cd7162877579
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1583a06db19ab8874ce7b3b985e3366ff74dcfffe4a7162db5c502d3582ee1b1a5328dc9f061caf863ca38d74e913d38114c9f73804616c4cef8cce10321fc4
|
7
|
+
data.tar.gz: 8d671a6476db8a8ed65da10b25a0201da57d4c66ab270b75f5bb6a921d7351bdb1a2e27f2ffe317f2c7bd07f0fe96a9a7ce6d71e7b40ce554149c6a6baa2d95d
|
data/documentation/usage.md
CHANGED
@@ -16,7 +16,7 @@ or, if with bundler
|
|
16
16
|
> bundle exec bcome
|
17
17
|
```
|
18
18
|
|
19
|
-
### Entering the shell and jumping to a quick link
|
19
|
+
### Entering the shell and jumping to a quick link context
|
20
20
|
|
21
21
|
```
|
22
22
|
> cd /your/installed/bcome/application/directory
|
@@ -119,17 +119,21 @@ e.g. remove 'identifier', OR remove ['array', 'of','identifiers']
|
|
119
119
|
> selections
|
120
120
|
- Show all added resources.
|
121
121
|
|
122
|
-
>
|
123
|
-
-
|
124
|
-
e.g.
|
122
|
+
> put
|
123
|
+
- Uploads files to all selected resources. Uses Rsync, and so copies recursively
|
124
|
+
e.g. put 'localpath', 'remotepath'
|
125
|
+
|
126
|
+
> get
|
127
|
+
- Downloads files from all remote resources down to local. Does so using Rsync, and so is recursive.
|
128
|
+
- A 'downloads' directory will be created in your project directory, within which downloaded files are stored.
|
129
|
+
e.g. get 'remotepath'
|
125
130
|
|
126
131
|
> run
|
127
132
|
- Execute a command on all selected resources
|
128
133
|
e.g. run 'command'
|
129
134
|
|
130
|
-
>
|
131
|
-
-
|
132
|
-
e.g. scp ['array','of', 'file', 'paths'], 'remote_path'
|
135
|
+
> sudo
|
136
|
+
- Enters 'sudo' mode, resulting in 'get' or 'put' commands being execute remotely using 'sudo'. This assumes that you have passwordless sudo setup on each respective remote host.
|
133
137
|
|
134
138
|
### Instance level commands
|
135
139
|
|
@@ -156,26 +160,22 @@ e.g. workon 'identifier'
|
|
156
160
|
- Execute a shell command on your local machine.
|
157
161
|
e.g. local "command"
|
158
162
|
|
159
|
-
>
|
160
|
-
-
|
161
|
-
e.g.
|
163
|
+
> put
|
164
|
+
- Uploads files. Uses Rsync, and so copies recursively
|
165
|
+
e.g. put 'localpath', 'remotepath'
|
166
|
+
|
167
|
+
> get
|
168
|
+
- Downloads files to local. Does so using Rsync, and so is recursive.
|
169
|
+
- A 'downloads' directory will be created in your project directory, within which downloaded files are stored.
|
170
|
+
e.g. get 'remotepath'
|
162
171
|
|
163
172
|
> run
|
164
173
|
- Execute a command.
|
165
174
|
e.g. run 'command'
|
166
175
|
|
167
|
-
>
|
168
|
-
-
|
169
|
-
e.g. scp ['array','of', 'file', 'paths'], 'remote_path'
|
176
|
+
> sudo
|
177
|
+
- Enters 'sudo' mode, resulting in 'get' or 'put' commands being execute remotely using 'sudo'. This assumes that you have passwordless sudo setup on the remote host.
|
170
178
|
|
171
179
|
> ssh
|
172
180
|
- Initiate an SSH connection.
|
173
181
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
data/lib/bcome.rb
CHANGED
@@ -5,5 +5,8 @@ require 'aws-sdk'
|
|
5
5
|
require 'net/scp'
|
6
6
|
require 'net/ssh/proxy/command'
|
7
7
|
require 'fog'
|
8
|
+
require 'require_all'
|
9
|
+
|
10
|
+
require_all "#{File.dirname(__FILE__)}/../lib"
|
11
|
+
require_all "#{File.dirname(__FILE__)}/../filters"
|
8
12
|
|
9
|
-
Dir[File.dirname(__FILE__) + "/**/*.rb"].each {|file| require file }
|
data/lib/bcome/version.rb
CHANGED
data/lib/become_object.rb
CHANGED
@@ -12,6 +12,15 @@ module ::Bcome::BecomeObject
|
|
12
12
|
"#{previous_workspace_object.send(:become_identifier)}> #{identifier}"
|
13
13
|
end
|
14
14
|
|
15
|
+
|
16
|
+
def local_download_path
|
17
|
+
"#{Dir.pwd}/downloads#{namespace}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def namespace
|
21
|
+
"#{previous_workspace_object.send(:namespace)}/#{identifier}"
|
22
|
+
end
|
23
|
+
|
15
24
|
def previous_workspace_object=(object)
|
16
25
|
@previous_workspace_object = object
|
17
26
|
end
|
data/lib/boot.rb
CHANGED
@@ -22,4 +22,12 @@ class ::Bcome::Boot
|
|
22
22
|
::START_PROMPT
|
23
23
|
end
|
24
24
|
|
25
|
+
def namespace
|
26
|
+
starting_namespace
|
27
|
+
end
|
28
|
+
|
29
|
+
def starting_namespace
|
30
|
+
"" # Used to determine where to store downloaded file - this is the start point directory, relative to the bcome install directory
|
31
|
+
end
|
32
|
+
|
25
33
|
end
|
@@ -31,8 +31,8 @@ module ::Bcome::EnvironmentSSH
|
|
31
31
|
|
32
32
|
def execute_scp_upload(files, remote_path, node)
|
33
33
|
begin
|
34
|
-
scp = ::Bcome::Scp.new(
|
35
|
-
scp.upload!
|
34
|
+
scp = ::Bcome::Scp.new(proxy, node)
|
35
|
+
scp.upload!(files, remote_path)
|
36
36
|
rescue Net::SSH::AuthenticationFailed
|
37
37
|
raise "Could not authenticate connection to #{node.identifier}".failure
|
38
38
|
rescue Net::SSH::Disconnect
|
@@ -40,5 +40,15 @@ module ::Bcome::EnvironmentSSH
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
def execute_scp_download(remote_path, local_path, node)
|
44
|
+
begin
|
45
|
+
scp = ::Bcome::Scp.new(proxy, node)
|
46
|
+
scp.download!(remote_path, local_path)
|
47
|
+
rescue Net::SSH::AuthenticationFailed
|
48
|
+
raise "Could not authenticate connection to #{node.identifier}".failure
|
49
|
+
rescue Net::SSH::Disconnect
|
50
|
+
raise "SSH connection to #{node.identifier} was disconnected".failure
|
51
|
+
end
|
52
|
+
end
|
43
53
|
|
44
54
|
end
|
@@ -9,19 +9,34 @@ module ::Bcome::InstanceCommand
|
|
9
9
|
@environment.execute_command(commands, self)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
12
|
+
def put(local_path, remote_path)
|
13
13
|
puts "rsync> #{self.identifier}".informational
|
14
14
|
if @environment.ssh_mode_type == ::SSH_DIRECT_MODE_IDENTIFIER
|
15
|
-
rsync_command = "rsync -av #{local_path} #{self.ssh_user}@#{self.ip_address}:#{remote_path}"
|
15
|
+
rsync_command = "rsync #{rsync_is_sudo}-av #{local_path} #{self.ssh_user}@#{self.ip_address}:#{remote_path}"
|
16
16
|
else
|
17
|
-
rsync_command = "rsync -av -e \"ssh -A #{self.ssh_user}@#{@environment.bastion_ip_address} ssh -o StrictHostKeyChecking=no\" #{local_path} #{self.ssh_user}@#{self.ip_address}:#{remote_path}"
|
17
|
+
rsync_command = "rsync -av -e \"ssh -A #{self.ssh_user}@#{@environment.bastion_ip_address} ssh -o StrictHostKeyChecking=no\" #{rsync_is_sudo}#{local_path} #{self.ssh_user}@#{self.ip_address}:#{remote_path}"
|
18
18
|
end
|
19
19
|
run_local(rsync_command)
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
22
|
+
def get(remote_path, local_path = local_download_path)
|
23
|
+
raise "No local path specified" unless local_path
|
24
|
+
raise "No remote_path specified" unless remote_path
|
25
|
+
|
26
|
+
if @environment.ssh_mode_type == ::SSH_DIRECT_MODE_IDENTIFIER
|
27
|
+
rsync_command = "rsync #{rsync_is_sudo} -chavzP #{self.ssh_user}@#{self.ip_address}:#{remote_path} #{local_path}"
|
28
|
+
else
|
29
|
+
rsync_command = "rsync -chavzP -av -e \"ssh -A #{self.ssh_user}@#{@environment.bastion_ip_address} ssh -o StrictHostKeyChecking=no\" #{rsync_is_sudo}#{self.ssh_user}@#{self.ip_address}:#{remote_path} #{local_path}"
|
30
|
+
end
|
31
|
+
|
32
|
+
silent = true
|
33
|
+
local("mkdir -p #{local_path}", silent)
|
34
|
+
run_local(rsync_command)
|
35
|
+
puts "done: files copied to #{local_path}".informational
|
36
|
+
end
|
37
|
+
|
38
|
+
def rsync_is_sudo
|
39
|
+
is_sudo? ? " --rsync-path=\"sudo rsync\" " : ""
|
25
40
|
end
|
26
41
|
|
27
42
|
end
|
data/lib/helpers/selections.rb
CHANGED
@@ -22,29 +22,28 @@ module ::Bcome::Selections
|
|
22
22
|
return
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
25
|
+
def put(local_path, remote_path)
|
26
26
|
if !@objects || @objects.empty?
|
27
27
|
no_selections_error
|
28
28
|
return
|
29
29
|
end
|
30
|
+
|
30
31
|
@objects.each do |object|
|
31
|
-
object.
|
32
|
+
object.put(local_path, remote_path)
|
32
33
|
end
|
33
34
|
return
|
34
35
|
end
|
35
36
|
|
36
|
-
def
|
37
|
+
def get(remote_path)
|
37
38
|
if !@objects || @objects.empty?
|
38
39
|
no_selections_error
|
39
40
|
return
|
40
41
|
end
|
41
|
-
|
42
|
-
return unless @objects
|
43
42
|
@objects.each do |object|
|
44
|
-
object.
|
45
|
-
end
|
43
|
+
object.get(remote_path)
|
44
|
+
end
|
46
45
|
return
|
47
|
-
end
|
46
|
+
end
|
48
47
|
|
49
48
|
def add(resource_identifier = nil)
|
50
49
|
if resource_identifier.is_a?(Array)
|
data/lib/object.rb
CHANGED
@@ -14,7 +14,21 @@ class Object
|
|
14
14
|
def become(object)
|
15
15
|
BECOME.set(object, self)
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
|
+
def toggle_sudo
|
19
|
+
@sudo = @sudo.nil? ? true : (@sudo ? false : true)
|
20
|
+
puts "sudo #{sudo_state.informational}"
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
def is_sudo?
|
25
|
+
@sudo
|
26
|
+
end
|
27
|
+
|
28
|
+
def sudo_state
|
29
|
+
is_sudo? ? "on" : "off"
|
30
|
+
end
|
31
|
+
|
18
32
|
def workon(identifier)
|
19
33
|
resource = resource_for_identifier(identifier)
|
20
34
|
|
@@ -32,7 +46,7 @@ class Object
|
|
32
46
|
{ :command => "list / ls", :description => "List all available resources at the current context." },
|
33
47
|
{ :command => "describe", :description => "Describe the resource object at the current context." },
|
34
48
|
{ :command => "workon' / w", :description => "Select a resource object, and switch to its context.", :usage => "workon 'identifier'" },
|
35
|
-
{ :command => "exit", :description => "
|
49
|
+
{ :command => "exit", :description => "Return to the previous context" },
|
36
50
|
{ :command => "exit!", :description => "Close all contexts, and exit Become."},
|
37
51
|
{ :command => "local", :description => "Execute a shell command on your local machine.", :usage => 'local "command"'}
|
38
52
|
]
|
@@ -62,7 +76,7 @@ class Object
|
|
62
76
|
def set_irb_prompt(conf)
|
63
77
|
conf[:PROMPT][:CUSTOM] = {
|
64
78
|
:PROMPT_N => "\e[1m:\e[m ",
|
65
|
-
:PROMPT_I => "\e[1m#{BECOME.irb_prompt} >\e[m ",
|
79
|
+
:PROMPT_I => "\e[1m#{BECOME.irb_prompt} #{is_sudo? ? " sudo ".danger : ""}>\e[m ", # high voltage
|
66
80
|
:PROMPT_C => "\e[1m#{BECOME.irb_prompt} >\e[m ",
|
67
81
|
:RETURN => ::VERBOSE ? "%s \n" : "\n"
|
68
82
|
}
|
data/lib/patches/string.rb
CHANGED
data/lib/scp.rb
CHANGED
@@ -1,18 +1,29 @@
|
|
1
1
|
class ::Bcome::Scp
|
2
2
|
|
3
|
-
|
4
|
-
@files = files
|
3
|
+
def initialize(proxy, node)
|
5
4
|
@proxy = proxy
|
6
5
|
@node = node
|
7
|
-
@remote_path = remote_path
|
8
6
|
end
|
9
7
|
|
10
|
-
def
|
8
|
+
def download!(remote_path, local_path = @node.local_download_path)
|
9
|
+
local("mkdir -p #{local_path}", true)
|
10
|
+
::Net::SCP.start(@node.ip_address, @node.ssh_user, { :proxy => @proxy, :keys => @node.keys, :paranoid => false }) do |scp|
|
11
|
+
scp.download!(remote_path, local_path, { :recursive => true, :preserve => true, :verbose => true }) do |cha, name, received, total|
|
12
|
+
pct = received / total.to_f
|
13
|
+
cnt = ( 72 * pct ).ceil
|
14
|
+
print "%-72s %.2f%%\r" % [ ("#" * cnt), ( pct * 100 ) ]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
puts "done: files copied to #{local_path}".informational
|
18
|
+
end
|
19
|
+
|
20
|
+
def upload!(files, remote_path)
|
11
21
|
puts "scp> #{@node.identifier}".informational
|
12
22
|
::Net::SCP.start(@node.ip_address, @node.ssh_user, { :proxy => @proxy, :keys => @node.keys, :paranoid => false }) do |scp|
|
13
|
-
|
23
|
+
files = files.is_a?(Array) ? files : [files]
|
24
|
+
files.each do |local_path|
|
14
25
|
name_old = ""
|
15
|
-
scp.upload!(local_path,
|
26
|
+
scp.upload!(local_path, remote_path, { :recursive => true }) do |ch, name, sent, total|
|
16
27
|
log_string = "#{name}: #{sent}/#{total}"
|
17
28
|
if name_old == name
|
18
29
|
STDOUT.write "\r#{log_string}"
|
data/lib/stack/environment.rb
CHANGED
@@ -31,9 +31,10 @@ module ::Bcome::Stack
|
|
31
31
|
{ :command => "remove", :description => "Remove a resource you no longer with to work on.", :usage => "remove 'identifier', OR remove ['array', 'of','identifiers']" },
|
32
32
|
{ :command => "clear!", :description => "Remove all selected resources." },
|
33
33
|
{ :command => "selections", :description => "Show all added resources." },
|
34
|
-
{ :command => "rsync", :description => "Rsync files to all selected resources.", :usage => "rsync 'localpath', 'remotepath'" },
|
35
34
|
{ :command => "run", :description => "Execute a command on all selected resources", :usage => "run 'command'" },
|
36
|
-
{ :command => "
|
35
|
+
{ :command => "get", :description => "Download from remote from all selected resources (recursive - uses rsync)", :usage => "get 'remote_path'" },
|
36
|
+
{ :command => "put", :description => "Copy files up to all selected resources (recursive - uses rsync)", :usage => "put 'local_path', 'remote_path'" },
|
37
|
+
{ :command => "sudo", :description => "Run 'get' and 'put' in sudo mode (assumes you have passwordless sudo setup)" }
|
37
38
|
]
|
38
39
|
end
|
39
40
|
|
@@ -51,6 +52,10 @@ module ::Bcome::Stack
|
|
51
52
|
return @meta_data[:ssh_mode][:ssh_user]
|
52
53
|
end
|
53
54
|
|
55
|
+
def sudo
|
56
|
+
toggle_sudo
|
57
|
+
end
|
58
|
+
|
54
59
|
def valid_ssh_modes
|
55
60
|
[::SSH_JUMP_MODE_IDENTIFIER, ::SSH_DIRECT_MODE_IDENTIFIER]
|
56
61
|
end
|
data/lib/stack/instance.rb
CHANGED
@@ -40,10 +40,11 @@ module ::Bcome::Stack
|
|
40
40
|
|
41
41
|
def menu_items
|
42
42
|
super + [
|
43
|
-
{ :command => "rsync", :description => "Rsync files.", :usage => "rsync 'localpath', 'remotepath'" },
|
44
43
|
{ :command => "run", :description => "Execute a command.", :usage => "run 'command'" },
|
45
|
-
{ :command => "
|
46
|
-
{ :command => "
|
44
|
+
{ :command => "put", :description => "Copy files to remote (recursive - uses rsync)", :usage => "put 'local_path', 'remote_path'" },
|
45
|
+
{ :command => "get", :description => "Download from remote (recursive - uses rsync).", :usage => "get 'remote_path'"},
|
46
|
+
{ :command => "ssh", :description => "Initiate an SSH connection." },
|
47
|
+
{ :command => "sudo", :description => "Run 'get' and 'put' in sudo mode (assumes you have passwordless sudo setup)" }
|
47
48
|
]
|
48
49
|
end
|
49
50
|
|
@@ -51,6 +52,18 @@ module ::Bcome::Stack
|
|
51
52
|
@environment.platform
|
52
53
|
end
|
53
54
|
|
55
|
+
def sudo
|
56
|
+
toggle_sudo
|
57
|
+
end
|
58
|
+
|
59
|
+
def namespace
|
60
|
+
"#{@environment.namespace}/#{identifier}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_sudo?
|
64
|
+
super || @environment.is_sudo?
|
65
|
+
end
|
66
|
+
|
54
67
|
def responds_to_list?
|
55
68
|
false
|
56
69
|
end
|
@@ -80,7 +93,7 @@ module ::Bcome::Stack
|
|
80
93
|
end
|
81
94
|
|
82
95
|
def do_describe
|
83
|
-
description = "#{identifier}"
|
96
|
+
description = "#{identifier}#{ is_sudo? ? " MODE: sudo " : "" }"
|
84
97
|
description += "\n\t* Internal IP #{@meta_data[:external_network_interface_address]}"
|
85
98
|
description += "\n\t* External IP #{public_ip_address}" if public_ip_address
|
86
99
|
description += "\n\t* #{role}" if role
|
data/lib/workspace_context.rb
CHANGED
@@ -1,19 +1,17 @@
|
|
1
|
-
class ::Bcome::WorkspaceContext
|
1
|
+
class ::Bcome::WorkspaceContext
|
2
2
|
|
3
3
|
attr_reader :object
|
4
4
|
|
5
5
|
def set(object, current_object, spawn = true)
|
6
|
-
|
7
6
|
@object = object
|
8
7
|
main_context = IRB.conf[:MAIN_CONTEXT]
|
9
8
|
@object.main_context = main_context.workspace if main_context
|
10
9
|
@object.previous_workspace_object = current_object if current_object
|
11
10
|
|
12
|
-
irb = IRB::Irb.new(IRB::WorkSpace.new(@object))
|
13
|
-
|
14
11
|
# Spawn is initiated when a user wants to shift workspace context.
|
15
12
|
# We don't spawn whilst setting up the hierarchy for quick contexts
|
16
13
|
spawn_for_object(@object) if spawn
|
14
|
+
return
|
17
15
|
end
|
18
16
|
|
19
17
|
def spawn_for_object(object)
|
@@ -33,4 +31,9 @@ class ::Bcome::WorkspaceContext ## TODO Move to include
|
|
33
31
|
def irb_prompt
|
34
32
|
@object ? @object.send(:become_identifier) : start_prompt
|
35
33
|
end
|
34
|
+
|
35
|
+
def is_sudo?
|
36
|
+
@object.is_sudo?
|
37
|
+
end
|
38
|
+
|
36
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bcome
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Roderick (Webzakimbo)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -86,10 +86,24 @@ dependencies:
|
|
86
86
|
- - '='
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: 1.33.0
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: require_all
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - '='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 1.3.3
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - '='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 1.3.3
|
89
103
|
description: Provides a console interface for traversing a hierarchy of platforms
|
90
104
|
-> environment -> servers, and exposes common administration tools for the managemenent
|
91
105
|
either of individual servers, or groups or servers. System is driven from simple
|
92
|
-
configuration, and is extensible. Integrates with AWS EC2 for dynamic network
|
106
|
+
configuration, and is extensible. Integrates with AWS EC2 for dynamic network discover.
|
93
107
|
email:
|
94
108
|
- guillaume@webzakimbo.com
|
95
109
|
executables:
|