phase 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in phase.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/phase ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "phase"
4
+ require "phase/cli"
5
+ ::Phase::CLI.new.run
@@ -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
@@ -0,0 +1,10 @@
1
+ require "phase/cli/command"
2
+
3
+ require "phase/cli/utils/loggable"
4
+
5
+ require "phase/cli/env"
6
+ require "phase/cli/keys"
7
+ require "phase/cli/logs"
8
+ require "phase/cli/mosh"
9
+ require "phase/cli/ssh"
10
+ require "phase/cli/status"
@@ -0,0 +1,18 @@
1
+ module Phase
2
+ module CLI
3
+ class Command
4
+
5
+ class << self
6
+ include ::Commander::Methods
7
+ end
8
+
9
+ attr_reader :args, :options
10
+
11
+ def initialize(args, options)
12
+ @args = args
13
+ @options = options
14
+ end
15
+
16
+ end
17
+ end
18
+ 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
@@ -0,0 +1,3 @@
1
+ module Phase
2
+ VERSION = "0.0.1"
3
+ 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: []