opsicle 0.1.0 → 0.2.0
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 +8 -8
- data/.gitignore +3 -0
- data/.travis.yml +6 -1
- data/Gemfile +3 -0
- data/Guardfile +5 -0
- data/README.markdown +44 -17
- data/bin/opsicle +36 -3
- data/lib/opsicle.rb +2 -3
- data/lib/opsicle/commands.rb +6 -0
- data/lib/opsicle/commands/deploy.rb +39 -0
- data/lib/opsicle/{list.rb → commands/list.rb} +0 -2
- data/lib/opsicle/{ssh.rb → commands/ssh.rb} +0 -3
- data/lib/opsicle/commands/ssh_key.rb +40 -0
- data/lib/opsicle/config.rb +1 -0
- data/lib/opsicle/deployment.rb +59 -0
- data/lib/opsicle/deployments.rb +22 -0
- data/lib/opsicle/monitor.rb +12 -0
- data/lib/opsicle/monitor/app.rb +147 -0
- data/lib/opsicle/monitor/panel.rb +98 -0
- data/lib/opsicle/monitor/panels/deployments.rb +42 -0
- data/lib/opsicle/monitor/panels/header.rb +48 -0
- data/lib/opsicle/monitor/panels/help.rb +33 -0
- data/lib/opsicle/monitor/screen.rb +83 -0
- data/lib/opsicle/monitor/spy/dataspyable.rb +19 -0
- data/lib/opsicle/monitor/spy/deployments.rb +53 -0
- data/lib/opsicle/monitor/subpanel.rb +55 -0
- data/lib/opsicle/monitor/translatable.rb +33 -0
- data/lib/opsicle/stack.rb +22 -0
- data/lib/opsicle/version.rb +1 -1
- data/opsicle.gemspec +1 -1
- data/spec/opsicle/client_spec.rb +6 -6
- data/spec/opsicle/commands/deploy_spec.rb +50 -0
- data/spec/opsicle/{list_spec.rb → commands/list_spec.rb} +7 -6
- data/spec/opsicle/commands/ssh_key_spec.rb +75 -0
- data/spec/opsicle/{ssh_spec.rb → commands/ssh_spec.rb} +24 -24
- data/spec/opsicle/config_spec.rb +12 -11
- data/spec/opsicle/monitor/app_spec.rb +63 -0
- data/spec/opsicle/monitor/panel_spec.rb +162 -0
- data/spec/opsicle/monitor/screen_spec.rb +121 -0
- data/spec/opsicle/monitor/spy/deployments_spec.rb +41 -0
- data/spec/opsicle/monitor/subpanel_spec.rb +199 -0
- data/spec/spec_helper.rb +2 -1
- metadata +44 -16
- data/Gemfile.lock +0 -75
- data/lib/opsicle/deploy.rb +0 -25
- data/spec/opsicle/deploy_spec.rb +0 -29
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZjkzMDgwYWI5OTI4NDQ5Mzc5MmRmZjg1ZTc3YWU3MTgxZWMxNTlmNA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YmNkNWQxZWFiOTg4MDgyMzU3NjA1ZDk4NTVhYzlhY2IzY2M5YzIwYg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NjgzMjk2ODIwMzRhOTk5ZjY5NmY2MWU1ZmEzYTEyMTZiMjJkYmE4Y2ZhYmJl
|
10
|
+
YThjNDNkYzUxMTI4NzlkZDI0MWQ4YWQ5MGJjMThjYjVkNTc1ZDM0YTFmMDBk
|
11
|
+
MTU5NDc0MWM4M2M4MWEzYWUwNTYzMzZkNzRhNzQwMTk3NzY4OWU=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NDJhYjQ3Y2IwZDBlYzRjY2I2MWZkNTRhNWRhZjVlNTNlOGY1ZDkzODE2Zjlh
|
14
|
+
ZTViNDdmNGYyYmY3ODc0MzdjYzU2ODg2YjQzYTJjNjgxNGRmMDE4YTA4OTIz
|
15
|
+
MmU5YTc1MTgxOTQ2MmJjMThlNzQ1ZmNkMzAzNjM3YWIxMjZkZTQ=
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Guardfile
ADDED
data/README.markdown
CHANGED
@@ -4,28 +4,31 @@ A gem bringing the glory of OpsWorks to your command line.
|
|
4
4
|
[](http://badge.fury.io/rb/opsicle)
|
5
5
|
[](https://travis-ci.org/sportngin/opsicle)
|
6
6
|
|
7
|
-
##
|
7
|
+
## Installation
|
8
|
+
Add this line to your project's Gemfile:
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
opsicle
|
12
|
-
|
13
|
-
|
14
|
-
opsicle deploy production
|
15
|
-
|
16
|
-
# SSH to a server instance in the given environment stack (ex: staging)
|
17
|
-
opsicle ssh staging
|
18
|
-
|
19
|
-
# Run other opsworks commands
|
20
|
-
opsicle update_custom_cookbooks staging
|
10
|
+
**For Ruby >=2.1.0**
|
11
|
+
```ruby
|
12
|
+
gem 'opsicle'
|
13
|
+
gem 'curses'
|
14
|
+
```
|
21
15
|
|
22
|
-
|
23
|
-
|
16
|
+
**For Ruby <2.1.0, 1.9.3**
|
17
|
+
```ruby
|
18
|
+
gem 'opsicle'
|
24
19
|
```
|
25
20
|
|
26
|
-
|
21
|
+
(Alternatively, `gem 'opsicle'; gem 'curses' unless RUBY_VERSION < "2.1.0"`)
|
27
22
|
|
28
|
-
|
23
|
+
**Why the extra `curses` gem for Ruby 2.1.0+?**
|
24
|
+
Opsicle uses [curses](http://en.wikipedia.org/wiki/Curses_(programming_library)).
|
25
|
+
Ruby's library to interface with curses was [removed from stdlib in Ruby 2.1.0](https://bugs.ruby-lang.org/issues/8584).
|
26
|
+
[The new curses gem](https://github.com/ruby/curses) is not backwards compatible, so in an effort to keep this gem
|
27
|
+
friendly with all current Ruby versions we don't list it as a dependency in Opsicle's gemspec - doing so would cause
|
28
|
+
errors for Ruby 1.9.3 users.
|
29
|
+
Ruby >=2.1.0 will likely be enforced sometime in 2014; [certainly by February 2015](https://www.ruby-lang.org/en/news/2014/01/10/ruby-1-9-3-will-end-on-2015/).
|
30
|
+
|
31
|
+
### Set up an Application to use opsicle
|
29
32
|
|
30
33
|
```yaml
|
31
34
|
# your_app_root/.opsicle
|
@@ -49,4 +52,28 @@ production:
|
|
49
52
|
aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
50
53
|
```
|
51
54
|
|
55
|
+
## Using Opsicle
|
56
|
+
|
57
|
+
Run `opsicle help` for a full list of commands and their uses.
|
58
|
+
Some common commands:
|
59
|
+
|
60
|
+
```bash
|
61
|
+
|
62
|
+
# Run a basic deploy for the current app
|
63
|
+
opsicle deploy staging
|
64
|
+
|
65
|
+
# Run the deploy for production
|
66
|
+
opsicle deploy production
|
52
67
|
|
68
|
+
# SSH to a server instance in the given environment stack
|
69
|
+
opsicle ssh staging
|
70
|
+
|
71
|
+
# Set your user SSH key (PUBLIC KEY) for OpsWorks
|
72
|
+
opsicle ssh-key staging <key-file>
|
73
|
+
|
74
|
+
# Launch the Opsicle Stack Monitor for the given environment stack
|
75
|
+
opsicle monitor staging
|
76
|
+
|
77
|
+
```
|
78
|
+
|
79
|
+
Opsicle accepts a `--verbose` flag to show additional information as commands are run.
|
data/bin/opsicle
CHANGED
@@ -16,7 +16,8 @@ default_command :help
|
|
16
16
|
|
17
17
|
command :deploy do |c|
|
18
18
|
c.syntax = "opsicle deploy <environment>"
|
19
|
-
c.description = "Deploy your current app to the given
|
19
|
+
c.description = "Deploy your current app to the given environment stack"
|
20
|
+
c.option "--browser", "Open OpsWorks deployments screen instead of Opsicle Stack Monitor"
|
20
21
|
c.action do |args, options|
|
21
22
|
raise ArgumentError, "Environment is required" unless args.first
|
22
23
|
Opsicle::Deploy.new(args.first).execute(options.__hash__)
|
@@ -25,7 +26,7 @@ end
|
|
25
26
|
|
26
27
|
command :list do |c|
|
27
28
|
c.syntax = "opsicle list <environment>"
|
28
|
-
c.description = "List all apps in the given environment"
|
29
|
+
c.description = "List all apps in the given environment stack"
|
29
30
|
c.action do |args, options|
|
30
31
|
raise ArgumentError, "Environment is required" unless args.first
|
31
32
|
Opsicle::List.new(args.first).execute(options.__hash__)
|
@@ -34,9 +35,41 @@ end
|
|
34
35
|
|
35
36
|
command :ssh do |c|
|
36
37
|
c.syntax = "opsicle ssh <environment>"
|
37
|
-
c.description = "SSH access to instances in the given
|
38
|
+
c.description = "SSH access to instances in the given environment stack"
|
38
39
|
c.action do |args, options|
|
39
40
|
raise ArgumentError, "Environment is required" unless args.first
|
40
41
|
Opsicle::SSH.new(args.first).execute(options.__hash__)
|
41
42
|
end
|
42
43
|
end
|
44
|
+
|
45
|
+
command 'ssh-key' do |c|
|
46
|
+
c.syntax = "opsicle ssh-key <environment> <key-file>"
|
47
|
+
c.description = "Set your user SSH key (PUBLIC KEY) for OpsWorks"
|
48
|
+
c.action do |args, options|
|
49
|
+
raise ArgumentError, "Environment is required" unless args.first
|
50
|
+
raise ArgumentError, "ssh public key-file is required" unless args[1]
|
51
|
+
Opsicle::SSHKey.new(*args).execute(options.__hash__)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
command 'monitor' do |c|
|
56
|
+
c.syntax = "opsicle monitor <environment>"
|
57
|
+
c.description = "Launch the Opsicle Stack Monitor for the given environment stack"
|
58
|
+
c.action do |args, options|
|
59
|
+
raise ArgumentError, "Environment is required" unless args.first
|
60
|
+
|
61
|
+
@monitor = Opsicle::Monitor::App.new(args.first, options.__hash__)
|
62
|
+
|
63
|
+
begin
|
64
|
+
@monitor.start
|
65
|
+
rescue => e
|
66
|
+
say "<%= color('Uh oh, an error occurred while starting the Opsicle Stack Monitor.', RED) %>"
|
67
|
+
say "<%= color('Use --trace to view stack trace.', RED) %>"
|
68
|
+
|
69
|
+
if options.trace
|
70
|
+
raise
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias_command :'top', :'monitor'
|
data/lib/opsicle.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Opsicle
|
2
|
+
class Deploy
|
3
|
+
attr_reader :client
|
4
|
+
|
5
|
+
def initialize(environment)
|
6
|
+
@environment = environment
|
7
|
+
@client = Client.new(environment)
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(options={})
|
11
|
+
response = client.run_command('deploy')
|
12
|
+
|
13
|
+
if options[:browser]
|
14
|
+
open_deploy(response[:deployment_id])
|
15
|
+
else
|
16
|
+
@monitor = Opsicle::Monitor::App.new(@environment, options)
|
17
|
+
|
18
|
+
begin
|
19
|
+
@monitor.start
|
20
|
+
rescue => e
|
21
|
+
say "<%= color('Uh oh, an error occurred while starting the Opsicle Stack Monitor.', RED) %>"
|
22
|
+
say "<%= color('Use --trace to view stack trace.', RED) %>"
|
23
|
+
|
24
|
+
if options.trace
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def open_deploy(deployment_id)
|
32
|
+
if deployment_id
|
33
|
+
exec "open 'https://console.aws.amazon.com/opsworks/home?#/stack/#{client.config.opsworks_config[:stack_id]}/deployments'"
|
34
|
+
else
|
35
|
+
puts 'deploy failed'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Opsicle
|
2
|
+
class SSHKey
|
3
|
+
|
4
|
+
def initialize(environment, keyfile)
|
5
|
+
@client = Client.new(environment)
|
6
|
+
@keyfile = keyfile
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(options={})
|
10
|
+
validate!
|
11
|
+
update
|
12
|
+
say "ssh-key updated successfully"
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
raise KeyFileNotFound, "No key file could be found" unless File.exists?(@keyfile)
|
17
|
+
raise InvalidKeyFile, "Key file is invalid" unless valid_key_file?
|
18
|
+
raise InvalidKeyFile, "Key file is a private key" unless public_key?
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid_key_file?
|
22
|
+
system("ssh-keygen -l -f #{@keyfile} > /dev/null")
|
23
|
+
end
|
24
|
+
|
25
|
+
def public_key?
|
26
|
+
!key.match(/PRIVATE KEY/)
|
27
|
+
end
|
28
|
+
|
29
|
+
def key
|
30
|
+
@key ||= File.read(@keyfile)
|
31
|
+
end
|
32
|
+
|
33
|
+
def update
|
34
|
+
@client.api_call(:update_my_user_profile, {ssh_public_key: key})
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
KeyFileNotFound = Class.new(StandardError)
|
39
|
+
InvalidKeyFile = Class.new(StandardError)
|
40
|
+
end
|
data/lib/opsicle/config.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Opsicle
|
2
|
+
class Deployment
|
3
|
+
|
4
|
+
def initialize(deployment_id, client)
|
5
|
+
@deployment_id = deployment_id
|
6
|
+
@client = client
|
7
|
+
end
|
8
|
+
|
9
|
+
def deployment_id
|
10
|
+
deployment[:deployment_id]
|
11
|
+
end
|
12
|
+
|
13
|
+
def stack_id
|
14
|
+
deployment[:stack_id]
|
15
|
+
end
|
16
|
+
|
17
|
+
def app_id
|
18
|
+
deployment[:app_id]
|
19
|
+
end
|
20
|
+
|
21
|
+
def created_at
|
22
|
+
deployment[:created_at]
|
23
|
+
end
|
24
|
+
|
25
|
+
def completed_at
|
26
|
+
deployment(reload: true)[:completed_at]
|
27
|
+
end
|
28
|
+
|
29
|
+
def duration
|
30
|
+
deployment(reload: true)[:duration]
|
31
|
+
end
|
32
|
+
|
33
|
+
def command
|
34
|
+
deployment[:command]
|
35
|
+
end
|
36
|
+
|
37
|
+
def status
|
38
|
+
deployment(reload: true)[:status]
|
39
|
+
end
|
40
|
+
|
41
|
+
def instance_ids
|
42
|
+
deployment[:instance_ids]
|
43
|
+
end
|
44
|
+
|
45
|
+
%w(running successful failed).each do |status_name|
|
46
|
+
define_method("#{status_name}?") { status == status_name }
|
47
|
+
end
|
48
|
+
|
49
|
+
def deployment(options={})
|
50
|
+
# Only call the API again if you need to
|
51
|
+
@deployment = nil if options[:reload]
|
52
|
+
@deployment ||= @client.api_call('describe_deployments',
|
53
|
+
:deployment_ids => [@deployment_id]
|
54
|
+
)[:deployments].first
|
55
|
+
end
|
56
|
+
private :deployment
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Opsicle
|
2
|
+
class Deployments
|
3
|
+
|
4
|
+
def initialize(client)
|
5
|
+
@client = client
|
6
|
+
end
|
7
|
+
|
8
|
+
def data
|
9
|
+
deployments(reload: true)
|
10
|
+
end
|
11
|
+
|
12
|
+
def deployments(options={})
|
13
|
+
# Only call the API again if you need to
|
14
|
+
@deployments = nil if options[:reload]
|
15
|
+
@deployments ||= @client.api_call('describe_deployments',
|
16
|
+
:stack_id => @client.config.opsworks_config[:stack_id]
|
17
|
+
)[:deployments]
|
18
|
+
end
|
19
|
+
private :deployments
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "opsicle/monitor/app"
|
2
|
+
require "opsicle/monitor/panel"
|
3
|
+
require "opsicle/monitor/subpanel"
|
4
|
+
require "opsicle/monitor/screen"
|
5
|
+
require "opsicle/monitor/translatable"
|
6
|
+
|
7
|
+
require "opsicle/monitor/panels/header"
|
8
|
+
require "opsicle/monitor/panels/deployments"
|
9
|
+
require "opsicle/monitor/panels/help"
|
10
|
+
|
11
|
+
require "opsicle/monitor/spy/dataspyable"
|
12
|
+
require "opsicle/monitor/spy/deployments"
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# Credit where credit is due:
|
2
|
+
# The Monitor module's architecture and many of its classes are heavily based on
|
3
|
+
# the work of tiredpixel's sidekiq-spy gem: https://github.com/tiredpixel/sidekiq-spy
|
4
|
+
# His help in working with the Ruby curses library has been invaluable - thanks tiredpixel!
|
5
|
+
|
6
|
+
require 'opsicle/client'
|
7
|
+
|
8
|
+
module Opsicle
|
9
|
+
module Monitor
|
10
|
+
class App
|
11
|
+
API_POLLING_INTERVAL = 10
|
12
|
+
SCREEN_REFRESH_INTERVAL = 5
|
13
|
+
|
14
|
+
attr_reader :running
|
15
|
+
attr_reader :restarting
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_accessor :client
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(environment, options)
|
22
|
+
@running = false
|
23
|
+
@restarting = false
|
24
|
+
@threads = {}
|
25
|
+
|
26
|
+
# Make client with correct configuration available to monitor spies
|
27
|
+
App.client = Client.new(environment)
|
28
|
+
end
|
29
|
+
|
30
|
+
def start
|
31
|
+
begin
|
32
|
+
@running = true
|
33
|
+
|
34
|
+
setup
|
35
|
+
|
36
|
+
@threads[:command] ||= Thread.new do
|
37
|
+
command_loop # listen for commands
|
38
|
+
end
|
39
|
+
|
40
|
+
@threads[:refresh_screen] ||= Thread.new do
|
41
|
+
refresh_screen_loop # refresh frequently
|
42
|
+
end
|
43
|
+
|
44
|
+
@threads[:refresh_data] ||= Thread.new do
|
45
|
+
refresh_data_loop # refresh not so frequently
|
46
|
+
end
|
47
|
+
|
48
|
+
@threads.each { |tname, t| t.join }
|
49
|
+
ensure
|
50
|
+
cleanup
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def stop
|
55
|
+
@running = false
|
56
|
+
@screen.close
|
57
|
+
@screen = nil # Ruby curses lib doesn't have closed?(), so we set to nil, just in case
|
58
|
+
|
59
|
+
exit 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def restart
|
63
|
+
@restarting = true
|
64
|
+
end
|
65
|
+
|
66
|
+
def do_command(key)
|
67
|
+
command = { q: :stop,
|
68
|
+
h: [:set_screen, :help],
|
69
|
+
b: :open_opsworks_browser,
|
70
|
+
d: [:set_screen, :deployments] }[key.to_sym]
|
71
|
+
command ||= :invalid_input
|
72
|
+
|
73
|
+
send *command unless command == :invalid_input
|
74
|
+
|
75
|
+
wakey_wakey # wake threads for immediate response
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def set_screen(screen)
|
81
|
+
@screen.panel_main = screen
|
82
|
+
end
|
83
|
+
|
84
|
+
def setup
|
85
|
+
@screen = Monitor::Screen.new
|
86
|
+
end
|
87
|
+
|
88
|
+
def cleanup
|
89
|
+
@screen.close if @screen
|
90
|
+
end
|
91
|
+
|
92
|
+
def wakey_wakey
|
93
|
+
@threads.each { |tname, t| t.run if t.status == 'sleep' }
|
94
|
+
end
|
95
|
+
|
96
|
+
def command_loop
|
97
|
+
while @running do
|
98
|
+
next unless @screen # #refresh_loop might be reattaching screen
|
99
|
+
|
100
|
+
key = @screen.next_key
|
101
|
+
|
102
|
+
next unless key # keep listening if timeout
|
103
|
+
|
104
|
+
do_command(key)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def refresh_screen_loop
|
109
|
+
while @running do
|
110
|
+
next unless @screen # HACK: only certain test scenarios?
|
111
|
+
|
112
|
+
if @restarting || @screen.missized? # signal(s) or whilst still resizing
|
113
|
+
panel_main = @screen.panel_main
|
114
|
+
|
115
|
+
cleanup
|
116
|
+
|
117
|
+
setup
|
118
|
+
|
119
|
+
@screen.panel_main = panel_main
|
120
|
+
|
121
|
+
@restarting = false
|
122
|
+
end
|
123
|
+
|
124
|
+
@screen.refresh
|
125
|
+
|
126
|
+
sleep SCREEN_REFRESH_INTERVAL # go to sleep; could be rudely awoken on quit
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# This loop is specifically separate from the screen loop
|
131
|
+
# because we don't want to spam OpWorks with API calls every second.
|
132
|
+
def refresh_data_loop
|
133
|
+
while @running do
|
134
|
+
next unless @screen # HACK: only certain test scenarios?
|
135
|
+
|
136
|
+
@screen.refresh_spies
|
137
|
+
|
138
|
+
sleep API_POLLING_INTERVAL
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def open_opsworks_browser
|
143
|
+
%x(open 'https://console.aws.amazon.com/opsworks/home?#/stack/#{App.client.config.opsworks_config[:stack_id]}')
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|