opsicle 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/opsicle.png)](http://badge.fury.io/rb/opsicle)
|
5
5
|
[![Build Status](https://travis-ci.org/sportngin/opsicle.png?branch=master)](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
|