phase 0.0.1
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 +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Makefile +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/VERSION +1 -0
- data/bin/phase +5 -0
- data/lib/phase/adapters/aws.rb +28 -0
- data/lib/phase/cli/all.rb +10 -0
- data/lib/phase/cli/command.rb +18 -0
- data/lib/phase/cli/env.rb +0 -0
- data/lib/phase/cli/keys.rb +175 -0
- data/lib/phase/cli/logs.rb +21 -0
- data/lib/phase/cli/mosh.rb +23 -0
- data/lib/phase/cli/ssh.rb +66 -0
- data/lib/phase/cli/status.rb +99 -0
- data/lib/phase/cli/utils/loggable.rb +21 -0
- data/lib/phase/cli.rb +16 -0
- data/lib/phase/configuration.rb +41 -0
- data/lib/phase/dsl.rb +28 -0
- data/lib/phase/keys/key.rb +91 -0
- data/lib/phase/ssh/backend.rb +32 -0
- data/lib/phase/ssh/command.rb +16 -0
- data/lib/phase/ssh/coordinator.rb +13 -0
- data/lib/phase/version.rb +3 -0
- data/lib/phase.rb +35 -0
- data/phase.gemspec +33 -0
- metadata +229 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 07ea8a370b008d8b134f8c30ba2c7887d6ac74e8
|
4
|
+
data.tar.gz: 8f26a343ffdb04f29c852815ed1ecbe9a8344515
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 41deb6339e385541f62e66efd4977dabe1d8283c76218dd8e3f7fdd118a9c144cbcf40f7628b037a7b0c597c7094d7cc9c104fff164a18c1a1bd3f18166d9bde
|
7
|
+
data.tar.gz: ef485f51fa20e53b096b50360d8dbc31bda58140434aba2674699b853de01cb90fe4965e7e56b2b2bae6bc57f0e43ecaede75ad119ea8bfa0fb9d68f23aadd1c
|
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 Orca Health, Inc.
|
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/Makefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
VERSION=$(shell cat VERSION)
|
2
|
+
|
3
|
+
.PHONY: default all clean install
|
4
|
+
default: clean all
|
5
|
+
all: build install alias
|
6
|
+
|
7
|
+
build: lib/phase/version.rb
|
8
|
+
gem build phase.gemspec
|
9
|
+
|
10
|
+
lib/phase/version.rb:
|
11
|
+
mkdir -p $(@D)
|
12
|
+
@echo 'module Phase\n VERSION = "$(VERSION)"\nend' > $@
|
13
|
+
|
14
|
+
install:
|
15
|
+
gem install phase-$(VERSION).gem --no-rdoc --no-ri
|
16
|
+
rbenv rehash
|
17
|
+
|
18
|
+
alias:
|
19
|
+
alias phase=/Users/piers/.rbenv/shims/phase
|
20
|
+
|
21
|
+
clean:
|
22
|
+
rm -rf lib/phase/version.rb
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Phase
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'phase'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install phase
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/[my-github-username]/phase/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/phase
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Phase
|
2
|
+
module Adapters
|
3
|
+
class AWS
|
4
|
+
# require 'capistrano/all'
|
5
|
+
require "fog/aws"
|
6
|
+
|
7
|
+
def find_servers(options = {})
|
8
|
+
query = {}
|
9
|
+
|
10
|
+
if options[:role]
|
11
|
+
query["tag:Role"] = options[:role]
|
12
|
+
end
|
13
|
+
|
14
|
+
ec2.servers.all(query).map do |h|
|
15
|
+
{
|
16
|
+
hostname: h.dns_name,
|
17
|
+
user: "orca"
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def ec2
|
23
|
+
@ec2 ||= ::Fog::Compute::AWS.new(region: ::Phase.config.aws_region)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
File without changes
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Phase
|
4
|
+
module Commands
|
5
|
+
class Keys < Command
|
6
|
+
|
7
|
+
command :keys do |c|
|
8
|
+
c.syntax = "phase keys [--grant email_address] [--revoke email_address [--delete]] [--role bastion_role]"
|
9
|
+
|
10
|
+
c.option "--grant email_address", String, "Add access for user with email_address."
|
11
|
+
c.option "--revoke email_address", String, "Revoke access for user with email_address."
|
12
|
+
c.option "--delete", "Delete key entry permanently. Defaults to false (comments-out instead)."
|
13
|
+
c.option "--role bastion_role", String, "Value of 'Role' tag for bastion hosts. Defaults to 'ssh'."
|
14
|
+
c.option "--list", "Default action. Lists email addresses and public keys of users with access."
|
15
|
+
|
16
|
+
c.description = "Adds or removes access by email address and public key on bastion servers."
|
17
|
+
c.action do |args, options|
|
18
|
+
options.default role: "ssh", delete: false
|
19
|
+
new(args, options).run
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
LOCKFILE_NAME = "phase.keys.lock"
|
24
|
+
|
25
|
+
def run
|
26
|
+
log "backing up existing keys..."
|
27
|
+
authorized_keys.backup!
|
28
|
+
|
29
|
+
if email = options.grant
|
30
|
+
add_key(email)
|
31
|
+
elsif email = options.revoke
|
32
|
+
remove_key(email, options.delete)
|
33
|
+
else
|
34
|
+
print_keys_list
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def authorized_keys
|
41
|
+
@authorized_keys ||= AuthorizedKeys.new(bastions)
|
42
|
+
end
|
43
|
+
|
44
|
+
def bastions
|
45
|
+
@bastions ||= begin
|
46
|
+
bastions = find_hosts(role: options.role)
|
47
|
+
fail "No bastions found with role '#{ role }'." unless bastions.any?
|
48
|
+
bastions
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def print_keys_list
|
53
|
+
table = ::Terminal::Table.new({
|
54
|
+
title: "Users with access",
|
55
|
+
headings: ["Email address", "Status", "Created at", "Updated at", "Public key"]
|
56
|
+
})
|
57
|
+
|
58
|
+
authorized_keys.each do |_, auth_key|
|
59
|
+
# Allow administrative keys to be hidden.
|
60
|
+
next if auth_key.status == "IGNORE"
|
61
|
+
|
62
|
+
cells = [
|
63
|
+
auth_key.email,
|
64
|
+
auth_key.status,
|
65
|
+
auth_key.created_at,
|
66
|
+
auth_key.updated_at,
|
67
|
+
auth_key.key_fragment
|
68
|
+
].map do |cell|
|
69
|
+
cell.to_s.send( auth_key.active? ? :green : :red )
|
70
|
+
end
|
71
|
+
|
72
|
+
table << cells
|
73
|
+
end
|
74
|
+
|
75
|
+
puts table
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_key(email)
|
79
|
+
if found_key = authorized_keys[email]
|
80
|
+
found_key.activate
|
81
|
+
else
|
82
|
+
key_text = ask("Enter public key:")
|
83
|
+
authorized_keys.add( Keys::Key.new(email: email, key: key_text) )
|
84
|
+
end
|
85
|
+
|
86
|
+
authorized_keys.persist!
|
87
|
+
|
88
|
+
print_keys_list
|
89
|
+
end
|
90
|
+
|
91
|
+
def remove_key(email, delete)
|
92
|
+
if found_key = authorized_keys[email]
|
93
|
+
if delete
|
94
|
+
authorized_keys.remove(found_key)
|
95
|
+
else
|
96
|
+
found_key.deactivate
|
97
|
+
end
|
98
|
+
|
99
|
+
authorized_keys.persist!
|
100
|
+
else
|
101
|
+
fail "No key found."
|
102
|
+
end
|
103
|
+
|
104
|
+
print_keys_list
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
class AuthorizedKeys
|
109
|
+
extend ::Forwardable
|
110
|
+
|
111
|
+
attr_reader :bastions
|
112
|
+
|
113
|
+
def initialize(bastions)
|
114
|
+
@bastions = bastions
|
115
|
+
@hash = Hash.new
|
116
|
+
fetch_keys
|
117
|
+
end
|
118
|
+
|
119
|
+
def fetch_keys
|
120
|
+
lines = raw_keys.lines.map(&:chomp).reject(&:empty?)
|
121
|
+
idx = 0
|
122
|
+
while idx < lines.count
|
123
|
+
attrs_line = lines[idx]
|
124
|
+
|
125
|
+
unless attrs_line.match(/\A### phase-key/)
|
126
|
+
idx += 1
|
127
|
+
next
|
128
|
+
end
|
129
|
+
|
130
|
+
key_line = lines[idx + 1]
|
131
|
+
add( Keys::Key.parse(attrs_line, key_line) )
|
132
|
+
idx += 2
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def add(key)
|
137
|
+
self[key.email] = key
|
138
|
+
end
|
139
|
+
|
140
|
+
def remove(key)
|
141
|
+
delete(key.email)
|
142
|
+
end
|
143
|
+
|
144
|
+
def to_s
|
145
|
+
reduce("") do |out, (_, key)|
|
146
|
+
out << key.to_s
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def persist!
|
151
|
+
log "writing new keys..."
|
152
|
+
bastions.exec("echo '#{to_s}' > ~/.ssh/authorized_keys")
|
153
|
+
end
|
154
|
+
|
155
|
+
def backup!
|
156
|
+
File.open( File.expand_path("~/.phase-keys"), "w" ) { |file| file << raw_keys }
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def raw_keys
|
162
|
+
@raw_keys ||= bastions.exec("cat ~/.ssh/authorized_keys").stdout
|
163
|
+
end
|
164
|
+
|
165
|
+
def reset!
|
166
|
+
@raw_keys = nil
|
167
|
+
@all = nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def_delegators :@hash, *(::Hash.instance_methods - AuthorizedKeys.instance_methods)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Phase
|
2
|
+
module Commands
|
3
|
+
class Logs < Command
|
4
|
+
|
5
|
+
command :logs do |c|
|
6
|
+
c.syntax = "phase logs [--tail]"
|
7
|
+
|
8
|
+
c.option "-t", "--tail", "Stream logs."
|
9
|
+
|
10
|
+
c.description = "."
|
11
|
+
c.action do |args, options|
|
12
|
+
new(args, options).run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Phase
|
2
|
+
module Commands
|
3
|
+
class Mosh < SSH
|
4
|
+
|
5
|
+
command :mosh do |c|
|
6
|
+
c.syntax = "phase mosh [-i instance_id] [-n instance_name] [-r instance_role] [-u user] [-c conn_str] [username@instance_name|instance_id]"
|
7
|
+
|
8
|
+
c.option "-i", "--id instance_id", String, "Connects to the instance with this ID."
|
9
|
+
c.option "-n", "--name instance_name", String, "Connects to the instance with this 'Name' tag."
|
10
|
+
c.option "-r", "--role instance_role", String, "Connects to an instance with this 'Role' tag. Default is 'ssh'."
|
11
|
+
c.option "-u", "--user username", String, "Remote username to connect with."
|
12
|
+
c.option "-c", "--conn conn_str", String, "Invokes conn_str to establish terminal session (e.g. --conn='ssh -i key.pem')."
|
13
|
+
|
14
|
+
c.description = "Connects to the the specified instance via mosh."
|
15
|
+
c.action do |args, options|
|
16
|
+
options.default role: "ssh", conn: "mosh --ssh='ssh -A'"
|
17
|
+
new(args, options).run
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Phase
|
2
|
+
module Commands
|
3
|
+
class SSH < Command
|
4
|
+
|
5
|
+
command :ssh do |c|
|
6
|
+
c.syntax = "phase ssh [-i instance_id] [-n instance_name] [-r instance_role] [-u user] [-c conn_str] [username@instance_name|instance_id]"
|
7
|
+
|
8
|
+
c.option "-i", "--id instance_id", String, "Connects to the instance with this ID."
|
9
|
+
c.option "-n", "--name instance_name", String, "Connects to the instance with this 'Name' tag."
|
10
|
+
c.option "-r", "--role instance_role", String, "Connects to an instance with this 'Role' tag. Default is 'ssh'."
|
11
|
+
c.option "-u", "--user username", String, "Remote username to connect with."
|
12
|
+
c.option "-c", "--conn conn_str", String, "Invokes conn_str to establish terminal session (e.g. --conn 'ssh -i key.pem')."
|
13
|
+
|
14
|
+
c.description = "Connects to the the specified instance via SSH."
|
15
|
+
c.action do |args, options|
|
16
|
+
options.default role: "ssh", conn: "ssh -A"
|
17
|
+
new(args, options).run
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :username, :instance
|
22
|
+
|
23
|
+
def run
|
24
|
+
parse_connection_string
|
25
|
+
log "connecting to instance #{ instance.id }..."
|
26
|
+
exec "#{ options.conn } #{ username }@#{ instance.dns_name }"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def parse_connection_string
|
32
|
+
# Handle "connection string" style parameter.
|
33
|
+
if conn = args.first
|
34
|
+
@username, str = conn.split("@")
|
35
|
+
fail "Malformed parameter: username@[instance-name|instance-id]." if @username.nil? || str.nil?
|
36
|
+
str.match(/i-[0-9a-f]+/i) ? options.id = str : options.name = str
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def username
|
41
|
+
@username ||= begin
|
42
|
+
user = options.user
|
43
|
+
fail "Must specify username with -u or 'username@[instance-name|instance-id]' parameter." if user.nil?
|
44
|
+
user
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def instance
|
49
|
+
@instance ||= begin
|
50
|
+
if options.id
|
51
|
+
instance = ec2.servers.all("instance-id" => options.id).first
|
52
|
+
elsif options.name
|
53
|
+
instance = ec2.servers.all("tag:Name" => options.name).first
|
54
|
+
else
|
55
|
+
instance = ec2.servers.all("tag:Role" => options.role).first
|
56
|
+
end
|
57
|
+
|
58
|
+
fail "no instance found." if instance.nil?
|
59
|
+
|
60
|
+
instance
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Phase
|
2
|
+
module Commands
|
3
|
+
class Status < Command
|
4
|
+
|
5
|
+
command :status do |c|
|
6
|
+
c.syntax = "phase status"
|
7
|
+
c.description = "Prints the current status of configured VPCs, subnets, and EC2 instances."
|
8
|
+
c.action do |args, options|
|
9
|
+
new(args, options).run
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
@vpcs = ec2.vpcs
|
15
|
+
@subnets = ec2.subnets
|
16
|
+
@servers = ec2.servers
|
17
|
+
@elbs = elb.load_balancers
|
18
|
+
|
19
|
+
print_vpc_tables
|
20
|
+
print_servers_table
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_vpc_tables
|
24
|
+
@vpcs.each do |vpc|
|
25
|
+
table = ::Terminal::Table.new(title: "VPC Status")
|
26
|
+
|
27
|
+
add_section_headers(table, ["VPC ID", "Name", "State", "CIDR Block", "Tenancy"])
|
28
|
+
color = vpc.state == "available" ? :green : :light_red
|
29
|
+
add_row(table, color, [
|
30
|
+
vpc.id,
|
31
|
+
vpc.tags["Name"] || vpc.tags["name"],
|
32
|
+
vpc.state,
|
33
|
+
vpc.cidr_block,
|
34
|
+
vpc.tenancy
|
35
|
+
])
|
36
|
+
|
37
|
+
subnets = @subnets.select do |subnet|
|
38
|
+
subnet.vpc_id == vpc.id
|
39
|
+
end
|
40
|
+
|
41
|
+
return unless subnets.any?
|
42
|
+
|
43
|
+
add_section_headers(table, ["Subnet ID", "Name", "State", "CIDR Block", "Availability Zone"])
|
44
|
+
subnets.each do |subnet|
|
45
|
+
color = subnet.ready? ? :green : :light_red
|
46
|
+
add_row(table, color, [
|
47
|
+
subnet.subnet_id,
|
48
|
+
subnet.tag_set["Name"] || subnet.tag_set["name"],
|
49
|
+
subnet.state,
|
50
|
+
subnet.cidr_block,
|
51
|
+
subnet.availability_zone
|
52
|
+
])
|
53
|
+
end
|
54
|
+
|
55
|
+
puts table
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def print_servers_table
|
60
|
+
table = ::Terminal::Table.new(title: "Instances")
|
61
|
+
groups = @servers.group_by(&:subnet_id)
|
62
|
+
|
63
|
+
add_section_headers(table, ["ID", "Name", "Type", "State", "Public IP", "Private IP", "Subnet Name"])
|
64
|
+
|
65
|
+
groups.each_pair do |subnet_id, servers|
|
66
|
+
servers.each do |server|
|
67
|
+
color = server.ready? ? :green : :light_red
|
68
|
+
subnet = @subnets.find { |s| s.subnet_id == subnet_id }
|
69
|
+
subnet_name = subnet.tag_set["Name"] || subnet.tag_set["name"] if subnet
|
70
|
+
|
71
|
+
add_row(table, color, [
|
72
|
+
server.id,
|
73
|
+
server.tags["Name"] || server.tags["name"],
|
74
|
+
server.flavor_id,
|
75
|
+
server.state,
|
76
|
+
server.public_ip_address,
|
77
|
+
server.private_ip_address,
|
78
|
+
subnet_name
|
79
|
+
])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
puts table
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def add_row(table, color_method, values)
|
89
|
+
table << values.map { |v| (v || "").send(color_method) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_section_headers(table, headers)
|
93
|
+
table.add_separator if table.number_of_columns > 0
|
94
|
+
table << headers
|
95
|
+
table.add_separator
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Phase
|
2
|
+
module CLI
|
3
|
+
module Utils
|
4
|
+
module Loggable
|
5
|
+
|
6
|
+
def log(str)
|
7
|
+
out = "[phase]".green
|
8
|
+
out << " #{ str }"
|
9
|
+
puts out
|
10
|
+
end
|
11
|
+
|
12
|
+
def fail!(str)
|
13
|
+
out = "[phase]".red
|
14
|
+
out << " #{ str }"
|
15
|
+
abort out
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/phase/cli.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "commander"
|
2
|
+
require "phase/cli/all"
|
3
|
+
|
4
|
+
module Phase
|
5
|
+
class CLI
|
6
|
+
include ::Commander::Methods
|
7
|
+
|
8
|
+
def run
|
9
|
+
program :name, "Phase"
|
10
|
+
program :version, ::Phase::VERSION
|
11
|
+
program :description, "Phase controller."
|
12
|
+
|
13
|
+
run!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Phase
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
attr_accessor :use_bastions,
|
5
|
+
:bastion_role,
|
6
|
+
:bastion_user,
|
7
|
+
|
8
|
+
:aws_region
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@use_bastions = false
|
12
|
+
@bastion_role = nil
|
13
|
+
@bastion_user = nil
|
14
|
+
|
15
|
+
@aws_region = "us-east-1"
|
16
|
+
|
17
|
+
::SSHKit.config.backend = SSH::Backend
|
18
|
+
|
19
|
+
configure_from_yml if defined?(::Rails) && yml_present?
|
20
|
+
end
|
21
|
+
|
22
|
+
def configure_from_yml
|
23
|
+
yml_config = ::YAML.load_file(yml_path) || {}
|
24
|
+
|
25
|
+
@use_bastions = yml_config[:use_bastions] if yml_config.has_key(:use_bastions)
|
26
|
+
@bastion_role = yml_config[:bastion_role] if yml_config.has_key(:bastion_role)
|
27
|
+
@bastion_user = yml_config[:bastion_user] if yml_config.has_key(:bastion_user)
|
28
|
+
|
29
|
+
@aws_region = yml_config[:aws_region] if yml_config.has_key(:aws_region)
|
30
|
+
end
|
31
|
+
|
32
|
+
def yml_present?
|
33
|
+
File.exists?(yml_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def yml_path
|
37
|
+
# ::Rails.root.join("config", "phase.yml")
|
38
|
+
""
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/phase/dsl.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Phase
|
2
|
+
module DSL
|
3
|
+
|
4
|
+
# def on_role(role_name, options = {}, &block)
|
5
|
+
# destination_ips = []
|
6
|
+
# on(destination_ips, options, &block)
|
7
|
+
# end
|
8
|
+
|
9
|
+
def on(destination_ips, options = {}, &block)
|
10
|
+
bastion_host = ["orca@54.165.207.98"]
|
11
|
+
|
12
|
+
coordinator = SSH::Coordinator.new(bastion_host)
|
13
|
+
|
14
|
+
destination_ips.each do |ip|
|
15
|
+
coordinator.each(options) do
|
16
|
+
on_remote_host(ip) { instance_exec(&block) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_locally(&block)
|
22
|
+
::SSHKit::Backend::Local.new(&block).run
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
include ::Phase::DSL
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Phase
|
2
|
+
module Keys
|
3
|
+
class Key
|
4
|
+
attr_accessor :email, :key, :created_at, :updated_at, :status
|
5
|
+
|
6
|
+
PATTERN_KEY_LINE = /ssh-rsa/
|
7
|
+
PATTERN_ATTRS_LINE = /\A### phase-key (.*)/
|
8
|
+
|
9
|
+
def self.parse(attrs_line, key_line)
|
10
|
+
if attrs_line.match(PATTERN_ATTRS_LINE)
|
11
|
+
attrs = JSON.parse($1)
|
12
|
+
|
13
|
+
new_key = new(
|
14
|
+
email: attrs["email"],
|
15
|
+
status: attrs["status"],
|
16
|
+
created_at: attrs["created_at"],
|
17
|
+
updated_at: attrs["updated_at"]
|
18
|
+
)
|
19
|
+
|
20
|
+
if key_line.match(PATTERN_KEY_LINE)
|
21
|
+
new_key.key = key_line.chomp
|
22
|
+
end
|
23
|
+
|
24
|
+
new_key
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(attrs = {})
|
29
|
+
self.email = attrs.fetch(:email)
|
30
|
+
self.key = attrs.fetch(:key, nil)
|
31
|
+
self.status = attrs.fetch(:status, "active")
|
32
|
+
self.created_at = attrs.fetch(:created_at, Time.now)
|
33
|
+
self.updated_at = attrs.fetch(:updated_at, Time.now)
|
34
|
+
end
|
35
|
+
|
36
|
+
def key=(text)
|
37
|
+
if text && !text.match(/\A(# )?ssh-rsa .+/)
|
38
|
+
raise ArgumentError, "Invalid key."
|
39
|
+
end
|
40
|
+
|
41
|
+
@key = text
|
42
|
+
end
|
43
|
+
|
44
|
+
def key_fragment
|
45
|
+
"..." + uncomment(self.key).split[1][-20..-1] if self.key
|
46
|
+
end
|
47
|
+
|
48
|
+
def attributes
|
49
|
+
{
|
50
|
+
email: self.email,
|
51
|
+
status: self.status,
|
52
|
+
created_at: self.created_at,
|
53
|
+
updated_at: self.updated_at
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def touch
|
58
|
+
self.updated_at = Time.now
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid?
|
62
|
+
!!self.email && !!self.key
|
63
|
+
end
|
64
|
+
|
65
|
+
def active?
|
66
|
+
self.status == "active"
|
67
|
+
end
|
68
|
+
|
69
|
+
def activate
|
70
|
+
self.status = "active"
|
71
|
+
self.key = uncomment(self.key)
|
72
|
+
end
|
73
|
+
|
74
|
+
def deactivate
|
75
|
+
self.status = "inactive"
|
76
|
+
self.key = comment(self.key)
|
77
|
+
end
|
78
|
+
|
79
|
+
def comment(key); "# #{key}"; end
|
80
|
+
def uncomment(key); self.key.sub(/\A# /, ""); end
|
81
|
+
|
82
|
+
def to_s
|
83
|
+
escape( ["", "### phase-key #{ ::JSON.dump(attributes) }", self.key, ""].join("\n") )
|
84
|
+
end
|
85
|
+
|
86
|
+
def escape(text)
|
87
|
+
text.gsub("'", "\'")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Phase
|
2
|
+
module SSH
|
3
|
+
class Backend < ::SSHKit::Backend::Netssh
|
4
|
+
include ::SSHKit::CommandHelper
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
# BUG: Backend::Netssh doesn't assign @pool when subclassed.
|
8
|
+
self.class.pool = ::SSHKit::Backend::ConnectionPool.new
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_remote_host(remote_host, &block)
|
13
|
+
@remote_host = remote_host
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def command(*args)
|
20
|
+
options = args.extract_options!
|
21
|
+
SSH::Command.new(*[ *args, options.merge({
|
22
|
+
in: @pwd.nil? ? nil : File.join(@pwd),
|
23
|
+
env: @env,
|
24
|
+
host: @host,
|
25
|
+
user: @user,
|
26
|
+
group: @group,
|
27
|
+
remote_host: @remote_host
|
28
|
+
}) ])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Phase
|
2
|
+
module SSH
|
3
|
+
class Command < ::SSHKit::Command
|
4
|
+
|
5
|
+
def on_remote_host(&block)
|
6
|
+
return yield unless options[:remote_host]
|
7
|
+
"ssh #{ options[:remote_host] } -- %s" % yield
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_command
|
11
|
+
on_remote_host { super }
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Phase
|
2
|
+
module SSH
|
3
|
+
class Coordinator < ::SSHKit::Coordinator
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
# Prevents Coordinator from uniqifing @raw_hosts.
|
8
|
+
def resolve_hosts
|
9
|
+
@raw_hosts.map { |rh| rh.is_a?(::SSHKit::Host) ? rh : ::SSHKit::Host.new(rh) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/phase.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "terminal-table"
|
2
|
+
require "active_support"
|
3
|
+
require "progressbar"
|
4
|
+
require "colorize"
|
5
|
+
require "fog"
|
6
|
+
require "sshkit"
|
7
|
+
|
8
|
+
require "dotenv"
|
9
|
+
::Dotenv.load if defined?(::Dotenv)
|
10
|
+
|
11
|
+
require "phase/adapters/aws"
|
12
|
+
|
13
|
+
require "phase/ssh/backend"
|
14
|
+
require "phase/ssh/command"
|
15
|
+
require "phase/ssh/coordinator"
|
16
|
+
|
17
|
+
require "phase/configuration"
|
18
|
+
require "phase/version"
|
19
|
+
|
20
|
+
|
21
|
+
module Phase
|
22
|
+
class << self
|
23
|
+
|
24
|
+
def config
|
25
|
+
@@config ||= Configuration.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset_config!
|
29
|
+
@@config = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
config
|
35
|
+
end
|
data/phase.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'phase/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "phase"
|
8
|
+
spec.version = Phase::VERSION
|
9
|
+
spec.authors = ["Piers Mainwaring", "Orca Health, Inc."]
|
10
|
+
spec.email = ["piers@impossibly.org"]
|
11
|
+
spec.summary = "Provides a simple API for managing cloud instances running in a multi-subnet network."
|
12
|
+
spec.description = ""
|
13
|
+
spec.homepage = "https://github.com/piersadrian/phase"
|
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_runtime_dependency 'commander', '~> 4.2'
|
22
|
+
spec.add_runtime_dependency 'terminal-table', '~> 1.4'
|
23
|
+
spec.add_runtime_dependency 'progressbar', '~> 0.21'
|
24
|
+
spec.add_runtime_dependency 'activesupport', '~> 4'
|
25
|
+
spec.add_runtime_dependency 'fog', '~> 1.23'
|
26
|
+
spec.add_runtime_dependency 'capistrano', '~> 3.2'
|
27
|
+
spec.add_runtime_dependency 'mina', '~> 0.3'
|
28
|
+
spec.add_runtime_dependency 'colorize', '~> 0.7'
|
29
|
+
spec.add_runtime_dependency 'dotenv', '~> 0.11'
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.1"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: phase
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Piers Mainwaring
|
8
|
+
- Orca Health, Inc.
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-12-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: commander
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.2'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4.2'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: terminal-table
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.4'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.4'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: progressbar
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0.21'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0.21'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: activesupport
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '4'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '4'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: fog
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '1.23'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.23'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: capistrano
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '3.2'
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.2'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: mina
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0.3'
|
105
|
+
type: :runtime
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0.3'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: colorize
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0.7'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0.7'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: dotenv
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0.11'
|
133
|
+
type: :runtime
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0.11'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: bundler
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - "~>"
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '1.6'
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - "~>"
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '1.6'
|
154
|
+
- !ruby/object:Gem::Dependency
|
155
|
+
name: rake
|
156
|
+
requirement: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - "~>"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '10.1'
|
161
|
+
type: :development
|
162
|
+
prerelease: false
|
163
|
+
version_requirements: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - "~>"
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '10.1'
|
168
|
+
description: ''
|
169
|
+
email:
|
170
|
+
- piers@impossibly.org
|
171
|
+
executables:
|
172
|
+
- phase
|
173
|
+
extensions: []
|
174
|
+
extra_rdoc_files: []
|
175
|
+
files:
|
176
|
+
- ".gitignore"
|
177
|
+
- Gemfile
|
178
|
+
- LICENSE.txt
|
179
|
+
- Makefile
|
180
|
+
- README.md
|
181
|
+
- Rakefile
|
182
|
+
- VERSION
|
183
|
+
- bin/phase
|
184
|
+
- lib/phase.rb
|
185
|
+
- lib/phase/adapters/aws.rb
|
186
|
+
- lib/phase/cli.rb
|
187
|
+
- lib/phase/cli/all.rb
|
188
|
+
- lib/phase/cli/command.rb
|
189
|
+
- lib/phase/cli/env.rb
|
190
|
+
- lib/phase/cli/keys.rb
|
191
|
+
- lib/phase/cli/logs.rb
|
192
|
+
- lib/phase/cli/mosh.rb
|
193
|
+
- lib/phase/cli/ssh.rb
|
194
|
+
- lib/phase/cli/status.rb
|
195
|
+
- lib/phase/cli/utils/loggable.rb
|
196
|
+
- lib/phase/configuration.rb
|
197
|
+
- lib/phase/dsl.rb
|
198
|
+
- lib/phase/keys/key.rb
|
199
|
+
- lib/phase/ssh/backend.rb
|
200
|
+
- lib/phase/ssh/command.rb
|
201
|
+
- lib/phase/ssh/coordinator.rb
|
202
|
+
- lib/phase/version.rb
|
203
|
+
- phase.gemspec
|
204
|
+
homepage: https://github.com/piersadrian/phase
|
205
|
+
licenses:
|
206
|
+
- MIT
|
207
|
+
metadata: {}
|
208
|
+
post_install_message:
|
209
|
+
rdoc_options: []
|
210
|
+
require_paths:
|
211
|
+
- lib
|
212
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
version: '0'
|
217
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
218
|
+
requirements:
|
219
|
+
- - ">="
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: '0'
|
222
|
+
requirements: []
|
223
|
+
rubyforge_project:
|
224
|
+
rubygems_version: 2.2.2
|
225
|
+
signing_key:
|
226
|
+
specification_version: 4
|
227
|
+
summary: Provides a simple API for managing cloud instances running in a multi-subnet
|
228
|
+
network.
|
229
|
+
test_files: []
|