wfa 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/README.md +101 -0
- data/Rakefile +28 -0
- data/bin/wfa +3 -0
- data/lib/wfa/command.rb +203 -0
- data/lib/wfa/console.rb +16 -0
- data/lib/wfa/device.rb +98 -0
- data/lib/wfa/generic_api.rb +97 -0
- data/lib/wfa/heartbeat.rb +14 -0
- data/lib/wfa/version.rb +3 -0
- data/lib/wfa.rb +8 -0
- data/man/wfa.1 +121 -0
- metadata +170 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
wfa(1) -- manage workfrom devices
|
2
|
+
=================================
|
3
|
+
|
4
|
+
## SYNOPSIS
|
5
|
+
|
6
|
+
wfa [-a|--show-all] [list]
|
7
|
+
wfa [-D|--dry-run] [shell] @<device-tag>
|
8
|
+
wfa [-D|--dry-run] [run] @<device-tag> <remote-command ...>
|
9
|
+
wfa identify <device-id> @<device-tag> <name>
|
10
|
+
wfa screen @<device-tag>
|
11
|
+
wfa heartbeats @<device-tag>
|
12
|
+
wfa console
|
13
|
+
|
14
|
+
## DESCRIPTION
|
15
|
+
|
16
|
+
`wfa` is a tool for listing and connecting to workfrom devices via SSH.
|
17
|
+
It is operated using sub-commands, but for simple use cases the sub-command is assumed.
|
18
|
+
|
19
|
+
## COMMANDS
|
20
|
+
|
21
|
+
### list
|
22
|
+
|
23
|
+
Display a list of registered devices.
|
24
|
+
Each device has a tag assigned for use in issuing commands.
|
25
|
+
This command is assumed if you provide no arguments.
|
26
|
+
Use `-a` to list untagged devices.
|
27
|
+
|
28
|
+
### shell
|
29
|
+
|
30
|
+
Open an interactive shell on the remote device, specified by the device tag.
|
31
|
+
This command is assumed if you provide a device tag without additional arguments.
|
32
|
+
|
33
|
+
### run
|
34
|
+
|
35
|
+
Issue a command on a device and display the output.
|
36
|
+
This command is assumed if you provide a device tag with an additional command argument.
|
37
|
+
|
38
|
+
### identify
|
39
|
+
|
40
|
+
Set a device tag and name, given the device ID.
|
41
|
+
Devices must be tagged in order to be maintained with wfa.
|
42
|
+
|
43
|
+
### screen
|
44
|
+
|
45
|
+
Open a screen window to a shell on the remote device, specified by the device tag.
|
46
|
+
Useful if you're running screen and want titled windows for each device.
|
47
|
+
|
48
|
+
### heartbeats
|
49
|
+
|
50
|
+
List the last 100 or so device heartbeats, specified by the device tag.
|
51
|
+
|
52
|
+
### console
|
53
|
+
|
54
|
+
Open an interactive Ruby prompt with a ready API handle.
|
55
|
+
It's useful for experimentation or exploring the API.
|
56
|
+
|
57
|
+
## USAGE
|
58
|
+
|
59
|
+
**Note** that you must be logged in to the tunnel server (as yourself) to use the
|
60
|
+
SSH tunneling features.
|
61
|
+
No authentication is performed, you must have SSH auth already configured to use it.
|
62
|
+
|
63
|
+
To see a list of devices:
|
64
|
+
|
65
|
+
wfa
|
66
|
+
|
67
|
+
To open a shell on a device:
|
68
|
+
|
69
|
+
wfa @ovation
|
70
|
+
|
71
|
+
To run a command on a device:
|
72
|
+
|
73
|
+
wfa @ovation cat /etc/hostname
|
74
|
+
|
75
|
+
To run these commands from your own machine, prepend
|
76
|
+
`ssh -A <login>@staging.workfrom.co` and they'll work more or less the same way.
|
77
|
+
(`-A` enables agent forwarding, which you need to access the devices themselves.)
|
78
|
+
|
79
|
+
## ADVANCED
|
80
|
+
|
81
|
+
### Installation notes
|
82
|
+
|
83
|
+
To install the gem you need to install some library dependencies.
|
84
|
+
For example, this works on Debian:
|
85
|
+
|
86
|
+
sudo apt-get install ruby-dev libcurl4-openssl-dev
|
87
|
+
|
88
|
+
### Distribution
|
89
|
+
|
90
|
+
Currently wfa is distributed via Zack's dropbox. The URL is:
|
91
|
+
|
92
|
+
https://dl.dropboxusercontent.com/u/16760254/wfa-latest.gem
|
93
|
+
|
94
|
+
To release a new version, Zack just builds the gem and copies to his dropbox public folder.
|
95
|
+
|
96
|
+
## FILES
|
97
|
+
|
98
|
+
### ~/.wfa.json
|
99
|
+
|
100
|
+
Saved settings for `wfa`, including login credentials and backend URL.
|
101
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems/tasks'
|
2
|
+
|
3
|
+
# I am dumb and keep forgetting to update the bundle before releasing
|
4
|
+
task :update_bundle do
|
5
|
+
system("bundle")
|
6
|
+
system("git ci -am 'update gemfile.lock'")
|
7
|
+
end
|
8
|
+
task :release => :update_bundle
|
9
|
+
|
10
|
+
require 'fileutils'
|
11
|
+
task :clean do
|
12
|
+
FileUtils.rm_rf %w[ pkg man ]
|
13
|
+
end
|
14
|
+
|
15
|
+
# process the README into a manual page using ronn
|
16
|
+
require 'ronn'
|
17
|
+
task :man do
|
18
|
+
print "Writing manual page..."
|
19
|
+
FileUtils.mkdir_p 'man'
|
20
|
+
File.open('man/wfa.1','w') do |man|
|
21
|
+
man.write Ronn::Document.new('README.md').to_roff
|
22
|
+
end
|
23
|
+
puts "done."
|
24
|
+
end
|
25
|
+
task "build:gem:wfa" => :man
|
26
|
+
|
27
|
+
Gem::Tasks.new
|
28
|
+
task :default => "build:gem:wfa"
|
data/bin/wfa
ADDED
data/lib/wfa/command.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'io/console'
|
3
|
+
require 'yajl'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
require 'wfa/generic_api'
|
7
|
+
|
8
|
+
class WFA::Command
|
9
|
+
def initialize args
|
10
|
+
@device_tag, @before_args, @after_args = extract_device_tag args
|
11
|
+
option_parser.permute!(@before_args)
|
12
|
+
|
13
|
+
@settings = {}
|
14
|
+
load_settings unless no_cache
|
15
|
+
|
16
|
+
@command =
|
17
|
+
if @device_tag.nil?
|
18
|
+
@before_args.shift || 'list'
|
19
|
+
else
|
20
|
+
if @before_args.count > 0
|
21
|
+
@before_args.shift
|
22
|
+
elsif @after_args.count > 0
|
23
|
+
"run"
|
24
|
+
else
|
25
|
+
"shell"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def start_wfa_command
|
31
|
+
case @command
|
32
|
+
when 'identify'
|
33
|
+
public_send @command, *@before_args, @device_tag, *@after_args
|
34
|
+
else
|
35
|
+
public_send @command, @device_tag, *@after_args
|
36
|
+
end
|
37
|
+
save_settings
|
38
|
+
rescue NoMethodError => e
|
39
|
+
$stderr.puts "no such command: #{@command}"
|
40
|
+
rescue Faraday::ClientError => e
|
41
|
+
$stderr.puts e.message
|
42
|
+
rescue StandardError => e
|
43
|
+
$stderr.puts e.message
|
44
|
+
end
|
45
|
+
|
46
|
+
def console *args
|
47
|
+
puts "Try e.g.: http.get('devices').body"
|
48
|
+
WFA::Console.new self, net
|
49
|
+
end
|
50
|
+
|
51
|
+
# list configured devices
|
52
|
+
def list *args
|
53
|
+
# force a reload of the device list
|
54
|
+
WFA::Device.fetch_and_cache_device_list(net)
|
55
|
+
puts WFA::Device.all(net).select {|device| show_all || device.tag }
|
56
|
+
end
|
57
|
+
|
58
|
+
# open a screen on the specified device
|
59
|
+
def screen device_tag, *args
|
60
|
+
device = WFA::Device.by_tag(net, device_tag) or raise "no such device"
|
61
|
+
shell_command "screen -t #{device.tag} -- ssh -A workfrom@#{device.last_ip_addr}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# open a shell on the specified device
|
65
|
+
def shell device_tag, *args
|
66
|
+
device = WFA::Device.by_tag(net, device_tag) or raise "no such device"
|
67
|
+
shell_command "ssh -A workfrom@#{device.last_ip_addr}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# issue a command on the specified device
|
71
|
+
def run device_tag, *args
|
72
|
+
device = WFA::Device.by_tag(net, device_tag) or raise "no such device"
|
73
|
+
shell_command "ssh -A workfrom@#{device.last_ip_addr} #{args.join(' ')}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def heartbeats device_tag, *args
|
77
|
+
device = WFA::Device.by_tag(net, device_tag) or raise "no such device"
|
78
|
+
puts device.heartbeats(net)
|
79
|
+
end
|
80
|
+
|
81
|
+
def identify port, device_tag, *args
|
82
|
+
device = WFA::Device.find(net, id)
|
83
|
+
if device.update_remote(net, tag:device_tag.sub(/^@/,''), name:args.join(' '))
|
84
|
+
puts 'updated!'
|
85
|
+
else
|
86
|
+
puts 'update failed :('
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
attr_accessor :dry_run, :base_url_option, :no_cache, :show_all, :login_option, :password_option
|
93
|
+
|
94
|
+
def load_settings
|
95
|
+
@settings = Yajl::Parser.parse(File.read("#{ENV['HOME']}/.wfa.json"), symbolize_keys:true)
|
96
|
+
rescue Errno::ENOENT, Yajl::ParseError
|
97
|
+
end
|
98
|
+
|
99
|
+
def save_settings
|
100
|
+
File.open("#{ENV['HOME']}/.wfa.json", "w").tap do |f|
|
101
|
+
f.write(Yajl::Encoder.encode(@settings))
|
102
|
+
f.close
|
103
|
+
FileUtils.chmod(0600, "#{ENV['HOME']}/.wfa.json")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def login_setting
|
108
|
+
@settings[:login] ||= ask("backend login: ")
|
109
|
+
end
|
110
|
+
|
111
|
+
def password_setting
|
112
|
+
@settings[:password] ||= ask_private("backend password: ")
|
113
|
+
end
|
114
|
+
|
115
|
+
def base_url_setting
|
116
|
+
@settings[:base_url] ||= ask("backend URL: ")
|
117
|
+
end
|
118
|
+
|
119
|
+
def ask prompt
|
120
|
+
print prompt
|
121
|
+
$stdin.readline.chomp
|
122
|
+
end
|
123
|
+
|
124
|
+
def ask_private prompt
|
125
|
+
print prompt
|
126
|
+
$stdin.noecho(&:readline).chomp.tap { puts }
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
def net_without_auth
|
131
|
+
GenericApi.connect(base_url_option||base_url_setting)
|
132
|
+
end
|
133
|
+
|
134
|
+
def net
|
135
|
+
@net ||= begin
|
136
|
+
login = login_option||login_setting
|
137
|
+
password = password_option||password_setting
|
138
|
+
url = base_url_option||base_url_setting
|
139
|
+
GenericApi.connect(url, login, password)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# execute a shell command. in dry run mode, just print the command to be executed.
|
144
|
+
def shell_command cmd
|
145
|
+
if dry_run
|
146
|
+
puts cmd
|
147
|
+
else
|
148
|
+
exec cmd
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# configure and return the option parser
|
153
|
+
def option_parser
|
154
|
+
self.base_url_option = ENV['WFDEVICE_BACKEND_URL']
|
155
|
+
self.login_option = nil
|
156
|
+
self.password_option = nil
|
157
|
+
self.no_cache = false
|
158
|
+
self.dry_run = false
|
159
|
+
self.show_all = false
|
160
|
+
OptionParser.new("Usage: wfa [-aCD] [<command>] [@<device_tag>] ...") do |opts|
|
161
|
+
opts.on("-u URL", "--base-url URL", "backend base URL") do |url|
|
162
|
+
self.base_url_option = url
|
163
|
+
end
|
164
|
+
opts.on("-l NAME", "--login NAME", "backend login name") do |name|
|
165
|
+
self.login_option = name
|
166
|
+
end
|
167
|
+
opts.on("-p PASS", "--p PASS", "backend password") do |pass|
|
168
|
+
self.password_option = pass
|
169
|
+
end
|
170
|
+
opts.on("-a", "--show-all", "Show untagged devices in listing") do
|
171
|
+
self.show_all = true
|
172
|
+
end
|
173
|
+
opts.on("-C", "--clear-settings", "Clear local settings before the operation") do
|
174
|
+
self.no_cache = true
|
175
|
+
end
|
176
|
+
opts.on("-D", "--dry-run", "Print the command that would be run without running it") do
|
177
|
+
self.dry_run = true
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# given the complete command arguments, find the device tag (if any)
|
183
|
+
# and return arrays of arguments both before and after the tag.
|
184
|
+
# If no tag is present all args are in the returned before args.
|
185
|
+
def extract_device_tag args
|
186
|
+
args = args.dup
|
187
|
+
tag = nil
|
188
|
+
before = []
|
189
|
+
after = []
|
190
|
+
args.each do |a|
|
191
|
+
if tag.nil?
|
192
|
+
if a.match(/^@[\w-]+$/)
|
193
|
+
tag = a
|
194
|
+
else
|
195
|
+
before << a
|
196
|
+
end
|
197
|
+
else
|
198
|
+
after << a
|
199
|
+
end
|
200
|
+
end
|
201
|
+
[tag, before, after]
|
202
|
+
end
|
203
|
+
end
|
data/lib/wfa/console.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
module WFA
|
5
|
+
class Console
|
6
|
+
attr_reader :wfa, :http
|
7
|
+
def initialize app, http
|
8
|
+
@wfa = app
|
9
|
+
@http = http
|
10
|
+
prompt = $PROGRAM_NAME.split('/').last + "> "
|
11
|
+
binding.pry quiet: true,
|
12
|
+
prompt:[->(a,b,c){ prompt }],
|
13
|
+
print:->(io, *ps){ ps.each {|p|PP.pp p, io, 100 } }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/wfa/device.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
class WFA::Device
|
6
|
+
WFDEVICE_PORT_FILE = '/etc/wfdevice_tunnel_port.sh'
|
7
|
+
FING_CSV_FILE = '/var/fing.csv'
|
8
|
+
|
9
|
+
ATTRIBUTES = [
|
10
|
+
:id, :name, :tag, :tunnel_port, :macaddr, :secret_key,
|
11
|
+
:state, :vendor, :hostname, :last_heartbeat, :last_ip_addr
|
12
|
+
]
|
13
|
+
attr_accessor(*ATTRIBUTES)
|
14
|
+
|
15
|
+
def initialize attrs
|
16
|
+
update attrs
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"@%s: %s (%d) [%s] %s" % [
|
21
|
+
tag||"---", name, id, last_ip_addr,
|
22
|
+
last_heartbeat ? time_ago(Time.parse(last_heartbeat)) : ''
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
def time_ago timestamp
|
27
|
+
seconds = (Time.now - timestamp).to_i
|
28
|
+
minutes = seconds / 60
|
29
|
+
return "#{seconds}s" unless minutes > 1
|
30
|
+
hours = minutes / 60
|
31
|
+
return "#{minutes}m" unless hours > 1
|
32
|
+
days = hours / 24
|
33
|
+
return "#{hours}h" unless days > 1
|
34
|
+
weeks = days / 7
|
35
|
+
return "#{days}d" unless weeks > 1
|
36
|
+
"#{weeks}w"
|
37
|
+
end
|
38
|
+
|
39
|
+
def heartbeats net
|
40
|
+
net.get(path("heartbeats")).body[:heartbeats].map{|h| WFA::Heartbeat.new(h) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def update_remote net, attrs
|
44
|
+
update attrs
|
45
|
+
net.put(path, name:name, tag:tag).success?
|
46
|
+
end
|
47
|
+
|
48
|
+
def update attrs
|
49
|
+
ATTRIBUTES.each { |attr|
|
50
|
+
if !attrs[attr].nil? && send(attr) != attrs[attr]
|
51
|
+
send("#{attr}=", attrs[attr])
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def path *args
|
57
|
+
(["devices/#{id}"]+args).join("/")
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def device_list_cache
|
62
|
+
"#{ENV['HOME']}/.wfa_device_list.json"
|
63
|
+
end
|
64
|
+
|
65
|
+
def by_tag net, tag
|
66
|
+
tag = tag.sub(/^@/,'')
|
67
|
+
all(net).detect {|device| device.tag == tag }
|
68
|
+
end
|
69
|
+
|
70
|
+
def find net, id
|
71
|
+
all(net).detect {|device| device.id == id }
|
72
|
+
end
|
73
|
+
|
74
|
+
def all net
|
75
|
+
@all ||= devices_list(net)[:devices].map do |device|
|
76
|
+
new device
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def devices_list net
|
81
|
+
get_cached_device_list || fetch_and_cache_device_list(net)
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_cached_device_list
|
85
|
+
File.exists?(device_list_cache) \
|
86
|
+
? Yajl::Parser.parse(File.read(device_list_cache), symbolize_keys:true) \
|
87
|
+
: raise('no cache!')
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch_and_cache_device_list net
|
91
|
+
net.get('devices').body.tap do |ds|
|
92
|
+
f = File.open(device_list_cache, "w")
|
93
|
+
f.write(Yajl::Encoder.encode(ds))
|
94
|
+
f.close
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
#
|
2
|
+
# This module is a starter Ruby client for your REST API, based on Faraday:
|
3
|
+
# https://github.com/lostisland/faraday/
|
4
|
+
#
|
5
|
+
# It supports HTTP basic authentication, sending and recieving JSON, and simple error reporting.
|
6
|
+
# I use this as a basis for building API clients in most of my Ruby projects.
|
7
|
+
#
|
8
|
+
# Use it like this:
|
9
|
+
#
|
10
|
+
# http = GenericApi.connect('http://localhost:3000/', 'myname', 'secret')
|
11
|
+
# http.get('boxes.json').body.each do |box|
|
12
|
+
# puts "found a box named #{box[:name]}! Let's put something inside."
|
13
|
+
# http.post('boxes/#{box[:id]}.json', contents: 'something!')
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# A few hints for getting started:
|
17
|
+
#
|
18
|
+
# This uses Patron as the HTTP client but you can change those constants to use
|
19
|
+
# another adapter or set to nil to use the built-in default.
|
20
|
+
#
|
21
|
+
# The response body is a Hash with Symbol keys (not String!).
|
22
|
+
# If you're using ActiveSupport, you should use a hash with indifferent access instead.
|
23
|
+
#
|
24
|
+
# The default MIME type is appropriate for most purposes, but some APIs use it for
|
25
|
+
# versioning so bear that in mind and change MIME_TYPE if necessary.
|
26
|
+
#
|
27
|
+
# Warmest regards,
|
28
|
+
#
|
29
|
+
# Zack Hobson <zack@zackhobson.com>
|
30
|
+
#
|
31
|
+
require 'faraday'
|
32
|
+
module GenericApi
|
33
|
+
FARADAY_ADAPTER = :patron
|
34
|
+
REQUIRE_DEPENDENCIES = ['patron']
|
35
|
+
MIME_TYPE = 'application/json'
|
36
|
+
|
37
|
+
# Connect and return a `Faraday::Connection` instance:
|
38
|
+
# http://rdoc.info/github/lostisland/faraday/Faraday/Connection
|
39
|
+
#
|
40
|
+
# If login and password are provided, the connection will use basic auth.
|
41
|
+
def self.connect url, login=nil, password=nil
|
42
|
+
Faraday.new(url) do |f|
|
43
|
+
f.use :generic_api, login, password
|
44
|
+
f.adapter(FARADAY_ADAPTER || Faraday.default_adapter)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Middleware < Faraday::Request::BasicAuthentication
|
49
|
+
Faraday::Middleware.register_middleware generic_api: ->{ self }
|
50
|
+
|
51
|
+
dependency do
|
52
|
+
require 'yajl'
|
53
|
+
(REQUIRE_DEPENDENCIES||[]).each {|dep| require dep }
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(env)
|
57
|
+
# encode with and accept json
|
58
|
+
env[:request_headers]['Accept'] = MIME_TYPE
|
59
|
+
env[:request_headers]['Content-Type'] = MIME_TYPE
|
60
|
+
env[:body] = Yajl::Encoder.encode(env[:body]) if env[:body]
|
61
|
+
|
62
|
+
# response processing
|
63
|
+
super(env).on_complete do |env|
|
64
|
+
begin
|
65
|
+
env[:body] = Yajl::Parser.parse(env[:body], symbolize_keys:true)
|
66
|
+
# XXX or, if you're using ActiveSupport:
|
67
|
+
# env[:body] = Yajl::Parser.parse(env[:body]).with_indifferent_access
|
68
|
+
rescue Yajl::ParseError
|
69
|
+
raise ApiError.new("Unable to parse the response:\n#{env[:body]}", env)
|
70
|
+
end
|
71
|
+
case env[:status]
|
72
|
+
when 300..399
|
73
|
+
raise RedirectError.new(env[:body][:message], env)
|
74
|
+
when 400..499
|
75
|
+
raise AuthError.new(env[:body][:message], env)
|
76
|
+
when 500..599
|
77
|
+
raise ApiError.new(env[:body][:message], env)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
class ApiError < Faraday::ClientError
|
83
|
+
def initialize(message, response)
|
84
|
+
super("Upstream API failure: #{message||'Internal error.'}", response)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
class AuthError < Faraday::ClientError
|
88
|
+
def initialize(message, response)
|
89
|
+
super("Authentication failure: #{message||'No reason given.'}", response)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
class RedirectError < Faraday::ClientError
|
93
|
+
def initialize(message, response)
|
94
|
+
super("Redirected: #{message||'No reason given.'}", response)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class WFA::Heartbeat
|
4
|
+
attr_reader :active_nodes, :created_at, :ip_addr
|
5
|
+
def initialize attrs
|
6
|
+
@ip_addr = attrs[:ip_addr]
|
7
|
+
@active_nodes = attrs[:active_nodes]
|
8
|
+
@created_at = Time.parse(attrs[:created_at])
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"%s: %d active nodes [%s]" % [ created_at.strftime("%F %T"), active_nodes, ip_addr ]
|
13
|
+
end
|
14
|
+
end
|
data/lib/wfa/version.rb
ADDED
data/lib/wfa.rb
ADDED
data/man/wfa.1
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
.\" generated with Ronn/v0.7.3
|
2
|
+
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
3
|
+
.
|
4
|
+
.TH "WFA" "1" "June 2014" "" ""
|
5
|
+
.
|
6
|
+
.SH "NAME"
|
7
|
+
\fBwfa\fR \- manage workfrom devices
|
8
|
+
.
|
9
|
+
.SH "SYNOPSIS"
|
10
|
+
.
|
11
|
+
.nf
|
12
|
+
|
13
|
+
wfa [\-a|\-\-show\-all] [list]
|
14
|
+
wfa [\-D|\-\-dry\-run] [shell] @<device\-tag>
|
15
|
+
wfa [\-D|\-\-dry\-run] [run] @<device\-tag> <remote\-command \.\.\.>
|
16
|
+
wfa identify <device\-id> @<device\-tag> <name>
|
17
|
+
wfa screen @<device\-tag>
|
18
|
+
wfa heartbeats @<device\-tag>
|
19
|
+
wfa console
|
20
|
+
.
|
21
|
+
.fi
|
22
|
+
.
|
23
|
+
.SH "DESCRIPTION"
|
24
|
+
\fBwfa\fR is a tool for listing and connecting to workfrom devices via SSH\. It is operated using sub\-commands, but for simple use cases the sub\-command is assumed\.
|
25
|
+
.
|
26
|
+
.SH "COMMANDS"
|
27
|
+
.
|
28
|
+
.SS "list"
|
29
|
+
Display a list of registered devices\. Each device has a tag assigned for use in issuing commands\. This command is assumed if you provide no arguments\. Use \fB\-a\fR to list untagged devices\.
|
30
|
+
.
|
31
|
+
.SS "shell"
|
32
|
+
Open an interactive shell on the remote device, specified by the device tag\. This command is assumed if you provide a device tag without additional arguments\.
|
33
|
+
.
|
34
|
+
.SS "run"
|
35
|
+
Issue a command on a device and display the output\. This command is assumed if you provide a device tag with an additional command argument\.
|
36
|
+
.
|
37
|
+
.SS "identify"
|
38
|
+
Set a device tag and name, given the device ID\. Devices must be tagged in order to be maintained with wfa\.
|
39
|
+
.
|
40
|
+
.SS "screen"
|
41
|
+
Open a screen window to a shell on the remote device, specified by the device tag\. Useful if you\'re running screen and want titled windows for each device\.
|
42
|
+
.
|
43
|
+
.SS "heartbeats"
|
44
|
+
List the last 100 or so device heartbeats, specified by the device tag\.
|
45
|
+
.
|
46
|
+
.SS "console"
|
47
|
+
Open an interactive Ruby prompt with a ready API handle\. It\'s useful for experimentation or exploring the API\.
|
48
|
+
.
|
49
|
+
.SH "USAGE"
|
50
|
+
\fBNote\fR that you must be logged in to the tunnel server (as yourself) to use the SSH tunneling features\. No authentication is performed, you must have SSH auth already configured to use it\.
|
51
|
+
.
|
52
|
+
.P
|
53
|
+
To see a list of devices:
|
54
|
+
.
|
55
|
+
.IP "" 4
|
56
|
+
.
|
57
|
+
.nf
|
58
|
+
|
59
|
+
wfa
|
60
|
+
.
|
61
|
+
.fi
|
62
|
+
.
|
63
|
+
.IP "" 0
|
64
|
+
.
|
65
|
+
.P
|
66
|
+
To open a shell on a device:
|
67
|
+
.
|
68
|
+
.IP "" 4
|
69
|
+
.
|
70
|
+
.nf
|
71
|
+
|
72
|
+
wfa @ovation
|
73
|
+
.
|
74
|
+
.fi
|
75
|
+
.
|
76
|
+
.IP "" 0
|
77
|
+
.
|
78
|
+
.P
|
79
|
+
To run a command on a device:
|
80
|
+
.
|
81
|
+
.IP "" 4
|
82
|
+
.
|
83
|
+
.nf
|
84
|
+
|
85
|
+
wfa @ovation cat /etc/hostname
|
86
|
+
.
|
87
|
+
.fi
|
88
|
+
.
|
89
|
+
.IP "" 0
|
90
|
+
.
|
91
|
+
.P
|
92
|
+
To run these commands from your own machine, prepend \fBssh \-A <login>@staging\.workfrom\.co\fR and they\'ll work more or less the same way\. (\fB\-A\fR enables agent forwarding, which you need to access the devices themselves\.)
|
93
|
+
.
|
94
|
+
.SH "ADVANCED"
|
95
|
+
.
|
96
|
+
.SS "Installation notes"
|
97
|
+
To install the gem you need to install some library dependencies\. For example, this works on Debian:
|
98
|
+
.
|
99
|
+
.IP "" 4
|
100
|
+
.
|
101
|
+
.nf
|
102
|
+
|
103
|
+
sudo apt\-get install ruby\-dev libcurl4\-openssl\-dev
|
104
|
+
.
|
105
|
+
.fi
|
106
|
+
.
|
107
|
+
.IP "" 0
|
108
|
+
.
|
109
|
+
.SS "Distribution"
|
110
|
+
Currently wfa is distributed via Zack\'s dropbox\. The URL is:
|
111
|
+
.
|
112
|
+
.P
|
113
|
+
https://dl\.dropboxusercontent\.com/u/16760254/wfa\-latest\.gem
|
114
|
+
.
|
115
|
+
.P
|
116
|
+
To release a new version, Zack just builds the gem and copies to his dropbox public folder\.
|
117
|
+
.
|
118
|
+
.SH "FILES"
|
119
|
+
.
|
120
|
+
.SS "~/\.wfa\.json"
|
121
|
+
Saved settings for \fBwfa\fR, including login credentials and backend URL\.
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wfa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Zack Hobson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-07-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: yajl-ruby
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: patron
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: faraday
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: pry
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rake
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: ronn
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rubygems-tasks
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Workfrom Admin tools
|
127
|
+
email: zack@zackhobson.com
|
128
|
+
executables:
|
129
|
+
- wfa
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- Rakefile
|
134
|
+
- Gemfile
|
135
|
+
- bin/wfa
|
136
|
+
- man/wfa.1
|
137
|
+
- README.md
|
138
|
+
- lib/wfa/command.rb
|
139
|
+
- lib/wfa/console.rb
|
140
|
+
- lib/wfa/device.rb
|
141
|
+
- lib/wfa/generic_api.rb
|
142
|
+
- lib/wfa/heartbeat.rb
|
143
|
+
- lib/wfa/version.rb
|
144
|
+
- lib/wfa.rb
|
145
|
+
homepage: http://workfrom.co/
|
146
|
+
licenses:
|
147
|
+
- private
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 1.8.23
|
167
|
+
signing_key:
|
168
|
+
specification_version: 3
|
169
|
+
summary: command-line tool for listing and connecting to workfrom devices via SSH
|
170
|
+
test_files: []
|