fleetctl 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +117 -0
- data/Rakefile +2 -0
- data/fleetctl.gemspec +26 -0
- data/lib/fleet/cluster.rb +78 -0
- data/lib/fleet/controller.rb +115 -0
- data/lib/fleet/discovery.rb +30 -0
- data/lib/fleet/item_set.rb +13 -0
- data/lib/fleet/machine.rb +33 -0
- data/lib/fleet/unit.rb +81 -0
- data/lib/fleetctl/command.rb +45 -0
- data/lib/fleetctl/options.rb +30 -0
- data/lib/fleetctl/remote_tempfile.rb +12 -0
- data/lib/fleetctl/runner/runner.rb +19 -0
- data/lib/fleetctl/runner/shell.rb +18 -0
- data/lib/fleetctl/runner/ssh.rb +51 -0
- data/lib/fleetctl/table_parser.rb +32 -0
- data/lib/fleetctl/version.rb +3 -0
- data/lib/fleetctl.rb +48 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cd025b42bf812102d80c3ded567acf7a5f3759bd
|
4
|
+
data.tar.gz: d51ee078697bbd24bbe9486d99f3521571ac99be
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c38239fb5798ad2616d91089e5f5d1633e368c67245fae29b1322fde08a725301af40182f3687a2d62a108df76c206ca9930148192961f8bfe031db7eea4875a
|
7
|
+
data.tar.gz: 7ab0ec882dbe5f63584898c5f7495e348e1634adb6bddaf124e19a5cab49f032cdb75fbfa6530c11cea35f17bf49d74253868c09bfc7af413c81035731289eb1
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Josh Lauer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# Fleetctl
|
2
|
+
|
3
|
+
Fleetctl is a gem for remotely controlling a fleet cluster on CoreOS. The gem executes all commands remotely on the fleet hosts via SSH, rather than using the `fleetctl` executable. At the time of this writing, this has proven to be more stable than using the native executable's `--tunnel` flag.
|
4
|
+
|
5
|
+
Warning: This gem is essentially a wrapper for an executable that is still in alpha. Things are changing rapidly. It is known to work in one specific context. Use at your own risk. Future updates will be more stable.
|
6
|
+
|
7
|
+
## Installation and Configuration
|
8
|
+
|
9
|
+
Add Fleetctl to your Gemfile:
|
10
|
+
|
11
|
+
gem 'fleetctl', git: 'https://github.com/josh-lauer/ruby-fleetctl', tag: 'v0.1.0'
|
12
|
+
|
13
|
+
And `$ bundle install`
|
14
|
+
|
15
|
+
###Configure Fleetctl:
|
16
|
+
|
17
|
+
All configuration options in Fleetctl are currently global. Just pass a hash to `Fleetctl.config` with the appropriate options included, like so:
|
18
|
+
|
19
|
+
Fleetctl.config({
|
20
|
+
# insert configuration options here
|
21
|
+
})
|
22
|
+
|
23
|
+
The options, with default values, are as follows:
|
24
|
+
|
25
|
+
# a hash of global flags to be passed to the fleetctl executable.
|
26
|
+
global: {}
|
27
|
+
|
28
|
+
# the path to the fleetctl executable on the fleet hosts. (go ahead and move or rename it, weirdo)
|
29
|
+
executable: 'fleetctl'
|
30
|
+
|
31
|
+
# the logger for fleet to use. Pass it Rails.logger if you are in a rails application
|
32
|
+
logger: Logger.new(STDOUT)
|
33
|
+
|
34
|
+
# a string or array of strings to be prepended to the fleetctl command to be executed
|
35
|
+
# on the remote fleet host. use this if you need to set environment variables and the like.
|
36
|
+
command_prefix: nil
|
37
|
+
|
38
|
+
# a discovery url to use to locate the fleet hosts.
|
39
|
+
discovery_url: nil
|
40
|
+
|
41
|
+
# the IP of any of the fleet hosts to use for discovery
|
42
|
+
fleet_host: nil
|
43
|
+
|
44
|
+
# the user to use when SSH'ing to the fleet hosts
|
45
|
+
fleet_user: 'core'
|
46
|
+
|
47
|
+
# options to pass varbatim to Net::SSH and Net::SCP
|
48
|
+
ssh_options: {}
|
49
|
+
|
50
|
+
# temp directory to be used on the fleet hosts
|
51
|
+
remote_temp_dir: '/tmp'
|
52
|
+
|
53
|
+
At a minumum, either :fleet_host or :discovery_url must be provided in order to contact the cluster. If both are used, the discovery_url will be used as a fallback if the fleet_host specified cannot be reached.
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
To use Fleetctl, first create a `Fleetctl::Controller`
|
57
|
+
|
58
|
+
A controller can be instantiated like this...
|
59
|
+
|
60
|
+
fleet = Fleetctl.new
|
61
|
+
=> #<Fleet::Controller...>
|
62
|
+
|
63
|
+
... or you can use a global singleton instance, like so:
|
64
|
+
|
65
|
+
fleet = Fleetctl.instance
|
66
|
+
=> #<Fleet::Controller...>
|
67
|
+
|
68
|
+
You can also call the methods `:instance`, `:machines`, `:units`, `:[]`, `:sync`, `:start`, `:submit`, `:load`, and `:destroy` on `Fleetctl` directly, and they will be passed to the singleton instance. More on them below.
|
69
|
+
|
70
|
+
In either case, Fleetctl caches its state in order to avoid repeatedly querying the cluster. When actions that would change the state are taken (such as publishing or deleting units) the cache is refreshed. In order to manually trigger a refresh, call:
|
71
|
+
|
72
|
+
fleet.sync
|
73
|
+
=> true
|
74
|
+
|
75
|
+
To get an array of all the units on the cluster:
|
76
|
+
|
77
|
+
fleet.units
|
78
|
+
=> [#<Fleet::Unit...>, #<Fleet::Unit...>, ...]
|
79
|
+
|
80
|
+
To get an array of all the machines that comprise the cluster:
|
81
|
+
|
82
|
+
fleet.machines
|
83
|
+
=> [#<Fleet::Machine...>, #<Fleet::Machine...>, ...]
|
84
|
+
|
85
|
+
To get a specific unit by name:
|
86
|
+
|
87
|
+
fleet['my-unit.service']
|
88
|
+
=> #<Fleet::Unit...>
|
89
|
+
|
90
|
+
The `:start`, `:load`, and `:submit` methods all operate on one or more `File` objects (fleet unitfiles).
|
91
|
+
unitfile = File.open('my-unit.service')
|
92
|
+
fleet.submit(unitfile)
|
93
|
+
=> true
|
94
|
+
|
95
|
+
To remove one or more units from the cluster, call `:destroy` on the controller, and pass in the name
|
96
|
+
or names of the units you wish to destroy
|
97
|
+
|
98
|
+
fleet.destroy('my-unit.service')
|
99
|
+
=> true
|
100
|
+
|
101
|
+
### Working with units
|
102
|
+
|
103
|
+
A `Fleet::Unit` represents a unitfile and its accompanying docker container, if any.
|
104
|
+
|
105
|
+
### Working with machines
|
106
|
+
|
107
|
+
A `Fleet::Machine` represents a machine which is part of a fleet cluster.
|
108
|
+
|
109
|
+
#### More Documentation pending
|
110
|
+
|
111
|
+
## Contributing
|
112
|
+
|
113
|
+
1. Fork it ( https://github.com/josh-lauer/fleetctl/fork )
|
114
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
115
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
116
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
117
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/fleetctl.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fleetctl/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'fleetctl'
|
8
|
+
spec.version = Fleetctl::VERSION
|
9
|
+
spec.authors = ['Josh Lauer']
|
10
|
+
spec.email = ['jlauer@cloudspace.com']
|
11
|
+
spec.summary = %q{A simple wrapper for fleetctl}
|
12
|
+
spec.description = %q{Allows controlling fleet clusters via a ruby API}
|
13
|
+
spec.homepage = 'https://github.com/josh-lauer/ruby-fleetctl'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.3'
|
23
|
+
spec.add_dependency 'hashie', '= 3.0.0'
|
24
|
+
spec.add_dependency 'net-ssh', '= 2.9.1'
|
25
|
+
spec.add_dependency 'net-scp', '= 1.2.1'
|
26
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Fleet
|
2
|
+
class Cluster < Fleet::ItemSet
|
3
|
+
attr_accessor :controller
|
4
|
+
|
5
|
+
def initialize(*args, controller: nil)
|
6
|
+
@controller = controller
|
7
|
+
super(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def fleet_hosts
|
11
|
+
map(&:ip)
|
12
|
+
end
|
13
|
+
|
14
|
+
def fleet_host
|
15
|
+
fleet_hosts.sample
|
16
|
+
end
|
17
|
+
|
18
|
+
def machines
|
19
|
+
discover! if empty?
|
20
|
+
to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
# attempts to rebuild the cluster from any of the hosts passed as arguments
|
24
|
+
# returns the first ip that worked, else nil
|
25
|
+
def build_from(*ip_addrs)
|
26
|
+
ip_addrs = [*ip_addrs].flatten.compact
|
27
|
+
begin
|
28
|
+
Fleetctl.logger.info 'building from hosts: ' + ip_addrs.inspect
|
29
|
+
built_from = ip_addrs.detect { |ip_addr| fetch_machines(ip_addr) }
|
30
|
+
Fleetctl.logger.info 'built successfully from host: ' + built_from.inspect if built_from
|
31
|
+
built_from
|
32
|
+
rescue => e
|
33
|
+
Fleetctl.logger.error 'ERROR building from hosts: ' + ip_addrs.inspect
|
34
|
+
Fleetctl.logger.error e.message
|
35
|
+
Fleetctl.logger.error e.backtrace.join("\n")
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# attempts a list-machines action on the given host.
|
41
|
+
# returns true if successful, else false
|
42
|
+
def fetch_machines(host)
|
43
|
+
Fleetctl.logger.info 'Fetching machines from host: '+host.inspect
|
44
|
+
clear
|
45
|
+
Fleetctl::Command.new('list-machines', '-l') do |runner|
|
46
|
+
runner.run(host: host)
|
47
|
+
new_machines = parse_machines(runner.output)
|
48
|
+
if runner.exit_code == 0
|
49
|
+
return true
|
50
|
+
else
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_machines(raw_table)
|
57
|
+
machine_hashes = Fleetctl::TableParser.parse(raw_table)
|
58
|
+
machine_hashes.map do |machine_attrs|
|
59
|
+
machine_attrs[:id] = machine_attrs.delete(:machine)
|
60
|
+
machine_attrs[:cluster] = self
|
61
|
+
add_or_find(Fleet::Machine.new(machine_attrs))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# attempts to rebuild the cluster by the specified fleet host, then hosts that it
|
66
|
+
# has built previously, and finally by using the discovery url
|
67
|
+
def discover!
|
68
|
+
known_hosts = [Fleetctl.options.fleet_host] | fleet_hosts.to_a
|
69
|
+
clear
|
70
|
+
success_host = build_from(known_hosts) || build_from(Fleet::Discovery.hosts)
|
71
|
+
if success_host
|
72
|
+
Fleetctl.logger.info 'Successfully recovered from host: ' + success_host.inspect
|
73
|
+
else
|
74
|
+
Fleetctl.logger.info 'Unable to recover!'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Fleet
|
2
|
+
class Controller
|
3
|
+
attr_writer :units
|
4
|
+
attr_accessor :cluster
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@cluster = Fleet::Cluster.new(controller: self)
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns an array of Fleet::Machine instances
|
11
|
+
def machines
|
12
|
+
cluster.machines
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns an array of Fleet::Unit instances
|
16
|
+
def units
|
17
|
+
return @units.to_a if @units
|
18
|
+
machines
|
19
|
+
fetch_units
|
20
|
+
@units.to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
# refreshes local state to match the fleet cluster
|
24
|
+
def sync
|
25
|
+
build_fleet
|
26
|
+
fetch_units
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# find a unitfile of a specific name
|
31
|
+
def [](unit_name)
|
32
|
+
units.detect { |u| u.name == unit_name }
|
33
|
+
end
|
34
|
+
|
35
|
+
# accepts one or more File objects, or an array of File objects
|
36
|
+
def start(*unit_file_or_files)
|
37
|
+
unitfiles = [*unit_file_or_files].flatten
|
38
|
+
out = unitfile_operation(:start, unitfiles)
|
39
|
+
clear_units
|
40
|
+
out
|
41
|
+
end
|
42
|
+
|
43
|
+
# accepts one or more File objects, or an array of File objects
|
44
|
+
def submit(*unit_file_or_files)
|
45
|
+
unitfiles = [*unit_file_or_files].flatten
|
46
|
+
out = unitfile_operation(:submit, unitfiles)
|
47
|
+
clear_units
|
48
|
+
out
|
49
|
+
end
|
50
|
+
|
51
|
+
# accepts one or more File objects, or an array of File objects
|
52
|
+
def load(*unit_file_or_files)
|
53
|
+
unitfiles = [*unit_file_or_files].flatten
|
54
|
+
out = unitfile_operation(:load, unitfiles)
|
55
|
+
clear_units
|
56
|
+
out
|
57
|
+
end
|
58
|
+
|
59
|
+
def destroy(*unit_names)
|
60
|
+
runner = Fleetctl::Command.run('destroy', unit_names)
|
61
|
+
clear_units
|
62
|
+
runner.exit_code == 0
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def build_fleet
|
68
|
+
cluster.discover!
|
69
|
+
end
|
70
|
+
|
71
|
+
def fleet_host
|
72
|
+
cluster.fleet_host
|
73
|
+
end
|
74
|
+
|
75
|
+
def clear_units
|
76
|
+
@units = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def unitfile_operation(command, files)
|
80
|
+
clear_units
|
81
|
+
if Fleetctl.options.runner_class.to_s == 'Shell'
|
82
|
+
runner = Fleetctl::Command.run(command.to_s, files.map(&:path))
|
83
|
+
else
|
84
|
+
runner = nil
|
85
|
+
Fleetctl::RemoteTempfile.open(*files) do |*remote_filenames|
|
86
|
+
runner = Fleetctl::Command.run(command.to_s, remote_filenames)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
runner.exit_code == 0
|
90
|
+
end
|
91
|
+
|
92
|
+
def fetch_units(host: fleet_host)
|
93
|
+
Fleetctl.logger.info 'Fetching units from host: '+host.inspect
|
94
|
+
@units = Fleet::ItemSet.new
|
95
|
+
Fleetctl::Command.new('list-units', '-l') do |runner|
|
96
|
+
runner.run(host: host)
|
97
|
+
parse_units(runner.output)
|
98
|
+
end
|
99
|
+
@units.to_a
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_units(raw_table)
|
103
|
+
unit_hashes = Fleetctl::TableParser.parse(raw_table)
|
104
|
+
unit_hashes.each do |unit_attrs|
|
105
|
+
if unit_attrs[:machine]
|
106
|
+
machine_id, machine_ip = unit_attrs[:machine].split('/')
|
107
|
+
unit_attrs[:machine] = cluster.add_or_find(Fleet::Machine.new(id: machine_id, ip: machine_ip))
|
108
|
+
end
|
109
|
+
unit_attrs[:name] = unit_attrs.delete(:unit)
|
110
|
+
unit_attrs[:controller] = self
|
111
|
+
@units.add_or_find(Fleet::Unit.new(unit_attrs))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fleet
|
2
|
+
class Discovery
|
3
|
+
class << self
|
4
|
+
def hosts
|
5
|
+
new(discovery_url).hosts
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :discovery_url
|
10
|
+
|
11
|
+
def initialize(discovery_url = Fleetctl.options.discovery_url)
|
12
|
+
@discovery_url = discovery_url
|
13
|
+
end
|
14
|
+
|
15
|
+
def data
|
16
|
+
@data ||= JSON.parse(Net::HTTP.get(URI.parse(@discovery_url)))
|
17
|
+
end
|
18
|
+
|
19
|
+
def hosts
|
20
|
+
begin
|
21
|
+
data['node']['nodes'].map{|node| node['value'].split(':')[0..1].join(':').split('//').last}
|
22
|
+
rescue => e
|
23
|
+
Fleetctl.logger.error 'ERROR in Fleet::Discovery#hosts, returning empty set'
|
24
|
+
Fleetctl.logger.error e.message
|
25
|
+
Fleetctl.logger.error e.backtrace.join("\n")
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Fleet
|
2
|
+
class Machine
|
3
|
+
attr_reader :cluster, :id, :ip, :metadata
|
4
|
+
|
5
|
+
def initialize(cluster: nil, id: nil, ip: nil, metadata: nil)
|
6
|
+
@cluster = cluster
|
7
|
+
@id = id
|
8
|
+
@ip = ip
|
9
|
+
@metadata = metadata
|
10
|
+
end
|
11
|
+
|
12
|
+
def controller
|
13
|
+
cluster.controller
|
14
|
+
end
|
15
|
+
|
16
|
+
def units
|
17
|
+
controller.units.select { |unit| unit.machine.id == id }
|
18
|
+
end
|
19
|
+
|
20
|
+
# run the command (string, array of command + args, whatever) and return stdout
|
21
|
+
def ssh(*command, port: 22)
|
22
|
+
runner = Fleetctl::Runner::SSH.new([*command].flatten.compact.join(' '))
|
23
|
+
runner.run(host: ip, ssh_options: { port: port })
|
24
|
+
runner.output
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other_machine)
|
28
|
+
id == other_machine.id && ip == other_machine.ip
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :eql?, :==
|
32
|
+
end
|
33
|
+
end
|
data/lib/fleet/unit.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Fleet
|
2
|
+
class Unit
|
3
|
+
# http://linuxrackers.com/doku.php?id=fedora_systemd_services
|
4
|
+
# LOAD = Reflects whether the unit definition was properly loaded.
|
5
|
+
# ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
|
6
|
+
# SUB = The low-level unit activation state, values depend on unit type.
|
7
|
+
attr_reader :controller, :name, :state, :load, :active, :sub, :desc, :machine
|
8
|
+
|
9
|
+
def initialize(controller:, name:, state:, load:, active:, sub:, desc:, machine:)
|
10
|
+
@controller = controller
|
11
|
+
@name = name
|
12
|
+
@state = state
|
13
|
+
@load = load
|
14
|
+
@active = active
|
15
|
+
@sub = sub
|
16
|
+
@machine = machine
|
17
|
+
end
|
18
|
+
|
19
|
+
[:status, :destroy, :stop, :start, :cat, :unload].each do |method_name|
|
20
|
+
define_method(method_name) do
|
21
|
+
cmd = Fleetctl::Command.new(method_name, self.name)
|
22
|
+
runner = cmd.run(host: ip)
|
23
|
+
runner.output
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def ip
|
28
|
+
machine && machine.ip
|
29
|
+
end
|
30
|
+
|
31
|
+
def creating?
|
32
|
+
active == 'activating' && sub == 'start-pre'
|
33
|
+
end
|
34
|
+
|
35
|
+
def failed?
|
36
|
+
active == 'failed' && sub == 'failed'
|
37
|
+
end
|
38
|
+
|
39
|
+
def running?
|
40
|
+
active == 'active' && sub == 'running'
|
41
|
+
end
|
42
|
+
|
43
|
+
# run the command on host (string, array of command + args, whatever) and return stdout
|
44
|
+
def ssh(*command, port: 22)
|
45
|
+
runner = Fleetctl::Runner::SSH.new([*command].flatten.compact.join(' '))
|
46
|
+
runner.run(host: ip, ssh_options: { port: port })
|
47
|
+
runner.output
|
48
|
+
end
|
49
|
+
|
50
|
+
# gets the external port mapped to port 22 inside this unit's docker container
|
51
|
+
# assumes that this unit corresponds to a docker container
|
52
|
+
def container_ssh_port(container_name = name)
|
53
|
+
return @container_ssh_port if defined? @container_ssh_port
|
54
|
+
docker_runner = Fleetctl::Runner::SSH.new('docker', 'port', container_name, 22)
|
55
|
+
docker_runner.run(host: ip)
|
56
|
+
@container_ssh_port = docker_runner.output.split(':').last.rstrip
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO: GET THIS WORKING
|
60
|
+
# # attempts to execute a command via ssh directly on the container
|
61
|
+
# # assumes that this unit corresponds to a docker container
|
62
|
+
# def container_ssh(*command, container_name: name, key: Dir.home+'/.ssh/id_rsa', username: 'root', password: nil)
|
63
|
+
# cmd_runner = Fleetctl::Runner::SSH.new([*command].flatten.compact.join(' '))
|
64
|
+
# cmd_runner.run(host: ip, ssh_options: { port: container_ssh_port(container_name), keys: [*key], username: username, password: password})
|
65
|
+
# runner.output
|
66
|
+
# end
|
67
|
+
|
68
|
+
# returns a JSON object representing the container
|
69
|
+
# assumes that this unit corresponds to a docker container
|
70
|
+
def docker_inspect(container_name = name)
|
71
|
+
raw = ssh('docker', 'inspect', container_name)
|
72
|
+
JSON.parse(raw)
|
73
|
+
end
|
74
|
+
|
75
|
+
def ==(other_unit)
|
76
|
+
name == other_unit.name
|
77
|
+
end
|
78
|
+
|
79
|
+
alias_method :eql?, :==
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
class Command
|
3
|
+
attr_accessor :command
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def run(*cmd, &blk)
|
7
|
+
obj = new(*cmd, &blk)
|
8
|
+
obj.run
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*cmd)
|
13
|
+
@command = cmd
|
14
|
+
yield(runner) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(*args)
|
18
|
+
runner.run(*args)
|
19
|
+
runner
|
20
|
+
end
|
21
|
+
|
22
|
+
def runner
|
23
|
+
klass = "Fleetctl::Runner::#{Fleetctl.options.runner_class}".constantize
|
24
|
+
@runner ||= klass.new(expression)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def global_options
|
30
|
+
Fleetctl.options.global.map { |k,v| "--#{k.to_s.gsub('_','-')}=#{v}" }
|
31
|
+
end
|
32
|
+
|
33
|
+
def prefix
|
34
|
+
Fleetctl.options.command_prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
def executable
|
38
|
+
Fleetctl.options.executable
|
39
|
+
end
|
40
|
+
|
41
|
+
def expression
|
42
|
+
[prefix, executable, global_options, command].flatten.compact.join(' ')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
class Options < Hashie::Mash
|
3
|
+
def initialize(*)
|
4
|
+
deep_merge!(Hashie::Mash.new(defaults))
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def defaults
|
9
|
+
{
|
10
|
+
global: {},
|
11
|
+
executable: 'fleetctl',
|
12
|
+
logger: Logger.new(STDOUT),
|
13
|
+
runner_class: 'SSH',
|
14
|
+
command_prefix: nil,
|
15
|
+
discovery_url: nil,
|
16
|
+
|
17
|
+
# for use with runner_class: 'SSH'
|
18
|
+
# these aren't used wih a Shell runner
|
19
|
+
fleet_host: nil,
|
20
|
+
fleet_user: 'core',
|
21
|
+
ssh_options: {},
|
22
|
+
remote_temp_dir: '/tmp'
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def ssh_options
|
27
|
+
self[:ssh_options].symbolize_keys
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
class RemoteTempfile
|
3
|
+
class << self
|
4
|
+
def open(local_file)
|
5
|
+
remote_path = File.join(Fleetctl.options.remote_temp_dir, File.basename(local_file.path))
|
6
|
+
Net::SCP.upload!(Fleetctl.options.fleet_host, Fleetctl.options.fleet_user, local_file.path, remote_path, :ssh => Fleetctl.options.ssh_options)
|
7
|
+
yield(remote_path)
|
8
|
+
Net::SSH.start(Fleetctl.options.fleet_host, Fleetctl.options.fleet_user, Fleetctl.options.ssh_options) { |ssh| ssh.exec!("rm #{remote_path}") }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
module Runner
|
3
|
+
class Runner
|
4
|
+
attr_reader :command, :status, :exit_code, :stdout_data, :stderr_data, :exit_signal
|
5
|
+
|
6
|
+
def initialize(*command)
|
7
|
+
@command = [*command].flatten.compact.join(' ')
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(*)
|
11
|
+
fail NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def output
|
15
|
+
@output || run
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
module Runner
|
3
|
+
class Shell < ::Fleetctl::Runner::Runner
|
4
|
+
def run(*)
|
5
|
+
return @output if @output
|
6
|
+
Fleetctl.logger.info "#{self.class.name} RUNNING: #{command}"
|
7
|
+
@stdout_data = `#{command}`
|
8
|
+
@status = $?
|
9
|
+
|
10
|
+
@exit_signal = @status.termsig
|
11
|
+
@exit_code = @status.exitstatus
|
12
|
+
Fleetctl.logger.info "EXIT CODE!: #{@exit_code.inspect}"
|
13
|
+
Fleetctl.logger.info "STDOUT: #{@output.inspect}"
|
14
|
+
@output = @stdout_data
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
module Runner
|
3
|
+
class SSH < ::Fleetctl::Runner::Runner
|
4
|
+
def run(host: Fleetctl.options.fleet_host, user: Fleetctl.options.fleet_user, ssh_options: {})
|
5
|
+
begin
|
6
|
+
ssh_options = Fleetctl.options.ssh_options.merge(ssh_options)
|
7
|
+
# return @output if @output
|
8
|
+
Fleetctl.logger.info "#{self.class.name} #{user}@#{host} RUNNING: #{command.inspect}"
|
9
|
+
Net::SSH.start(host, user, ssh_options) do |ssh|
|
10
|
+
@stdout_data = ''
|
11
|
+
@stderr_data = ''
|
12
|
+
@exit_code = nil
|
13
|
+
@exit_signal = nil
|
14
|
+
ssh.open_channel do |channel|
|
15
|
+
channel.exec(command) do |ch, success|
|
16
|
+
unless success
|
17
|
+
abort "FAILED: couldn't execute command (ssh.channel.exec)"
|
18
|
+
end
|
19
|
+
channel.on_data do |ch,data|
|
20
|
+
@stdout_data+=data
|
21
|
+
end
|
22
|
+
|
23
|
+
channel.on_extended_data do |ch,type,data|
|
24
|
+
@stderr_data+=data
|
25
|
+
end
|
26
|
+
|
27
|
+
channel.on_request('exit-status') do |ch,data|
|
28
|
+
@exit_code = data.read_long
|
29
|
+
end
|
30
|
+
|
31
|
+
channel.on_request('exit-signal') do |ch, data|
|
32
|
+
@exit_signal = data.read_long
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
ssh.loop
|
37
|
+
@output = @stdout_data
|
38
|
+
end
|
39
|
+
Fleetctl.logger.info "EXIT CODE!: #{exit_code.inspect}"
|
40
|
+
Fleetctl.logger.info "STDOUT: #{@output.inspect}"
|
41
|
+
@output
|
42
|
+
rescue => e
|
43
|
+
Fleetctl.logger.error 'ERROR in Runner#run'
|
44
|
+
Fleetctl.logger.error e.message
|
45
|
+
Fleetctl.logger.error e.backtrace.join("\n")
|
46
|
+
raise e
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Fleetctl
|
2
|
+
class TableParser
|
3
|
+
attr_accessor :raw
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def parse(raw)
|
7
|
+
self.new(raw).parse
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(raw)
|
12
|
+
@raw = raw
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse
|
16
|
+
rows = raw.split("\n").map { |row| row.split(/\t+/) }
|
17
|
+
header = rows.shift
|
18
|
+
if header
|
19
|
+
keys = header.map { |key| key.downcase.to_sym }
|
20
|
+
[].tap do |output|
|
21
|
+
rows.each do |row|
|
22
|
+
scrubbed_row = row.map { |val| val == '-' ? nil : val }
|
23
|
+
output << Hash[keys.zip(scrubbed_row)]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
else
|
27
|
+
Fleetctl.logger.error('ERROR in Fleetctl::TableParser.parse - no header row found')
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/fleetctl.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/scp'
|
3
|
+
require 'hashie'
|
4
|
+
|
5
|
+
require 'fleetctl/version'
|
6
|
+
require 'fleetctl/command'
|
7
|
+
require 'fleetctl/runner/runner'
|
8
|
+
require 'fleetctl/runner/ssh'
|
9
|
+
require 'fleetctl/runner/shell'
|
10
|
+
require 'fleetctl/table_parser'
|
11
|
+
require 'fleetctl/options'
|
12
|
+
require 'fleetctl/remote_tempfile'
|
13
|
+
|
14
|
+
require 'fleet/item_set'
|
15
|
+
require 'fleet/unit'
|
16
|
+
require 'fleet/machine'
|
17
|
+
require 'fleet/controller'
|
18
|
+
require 'fleet/discovery'
|
19
|
+
require 'fleet/cluster'
|
20
|
+
|
21
|
+
module Fleetctl
|
22
|
+
class << self
|
23
|
+
extend Forwardable
|
24
|
+
def_delegators :instance, :machines, :units, :[], :sync, :start, :submit, :load, :destroy
|
25
|
+
|
26
|
+
attr_reader :options
|
27
|
+
|
28
|
+
# use if you might need more than one fleet
|
29
|
+
def new(*args)
|
30
|
+
Fleet::Controller.new(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
# get the global singleton controller
|
34
|
+
def instance
|
35
|
+
@instance ||= Fleet::Controller.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# set global configuration options
|
39
|
+
def config(cfg)
|
40
|
+
@options = Options.new(cfg)
|
41
|
+
end
|
42
|
+
|
43
|
+
# set the logger for fleet to use
|
44
|
+
def logger
|
45
|
+
options.logger
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fleetctl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josh Lauer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hashie
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: net-ssh
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.9.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.9.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: net-scp
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.2.1
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.2.1
|
83
|
+
description: Allows controlling fleet clusters via a ruby API
|
84
|
+
email:
|
85
|
+
- jlauer@cloudspace.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- fleetctl.gemspec
|
96
|
+
- lib/fleet/cluster.rb
|
97
|
+
- lib/fleet/controller.rb
|
98
|
+
- lib/fleet/discovery.rb
|
99
|
+
- lib/fleet/item_set.rb
|
100
|
+
- lib/fleet/machine.rb
|
101
|
+
- lib/fleet/unit.rb
|
102
|
+
- lib/fleetctl.rb
|
103
|
+
- lib/fleetctl/command.rb
|
104
|
+
- lib/fleetctl/options.rb
|
105
|
+
- lib/fleetctl/remote_tempfile.rb
|
106
|
+
- lib/fleetctl/runner/runner.rb
|
107
|
+
- lib/fleetctl/runner/shell.rb
|
108
|
+
- lib/fleetctl/runner/ssh.rb
|
109
|
+
- lib/fleetctl/table_parser.rb
|
110
|
+
- lib/fleetctl/version.rb
|
111
|
+
homepage: https://github.com/josh-lauer/ruby-fleetctl
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.2.2
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: A simple wrapper for fleetctl
|
135
|
+
test_files: []
|