sonic-screwdriver 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +26 -2
- data/README.md +156 -23
- data/bin/sonic +8 -1
- data/docs/.gitignore +4 -0
- data/docs/CNAME +1 -0
- data/docs/Gemfile +3 -0
- data/docs/LICENSE +21 -0
- data/docs/README.md +21 -0
- data/docs/_config.yml +69 -0
- data/docs/_docs/commands.md +10 -0
- data/docs/_docs/how-it-works.md +34 -0
- data/docs/_docs/install.md +75 -0
- data/docs/_docs/next-steps.md +18 -0
- data/docs/_docs/settings.md +73 -0
- data/docs/_docs/sonic-ecs-exec.md +7 -0
- data/docs/_docs/sonic-ecs-run.md +7 -0
- data/docs/_docs/sonic-execute.md +7 -0
- data/docs/_docs/sonic-help.md +7 -0
- data/docs/_docs/sonic-list.md +7 -0
- data/docs/_docs/sonic-ssh.md +7 -0
- data/docs/_docs/tutorial-ecs-exec.md +69 -0
- data/docs/_docs/tutorial-ecs-run.md +94 -0
- data/docs/_docs/tutorial-execute.md +38 -0
- data/docs/_docs/tutorial-ssh.md +119 -0
- data/docs/_docs/tutorial.md +11 -0
- data/docs/_docs/why.md +27 -0
- data/docs/_includes/about.html +19 -0
- data/docs/_includes/commands.html +28 -0
- data/docs/_includes/contact.html +17 -0
- data/docs/_includes/contact_disqus.html +16 -0
- data/docs/_includes/contact_static.html +17 -0
- data/docs/_includes/content.html +21 -0
- data/docs/_includes/css/bootstrap.min.css +7 -0
- data/docs/_includes/css/main.css +481 -0
- data/docs/_includes/css/quotes.css +102 -0
- data/docs/_includes/css/sonic.css +163 -0
- data/docs/_includes/css/syntax.css +60 -0
- data/docs/_includes/css/table.css +53 -0
- data/docs/_includes/css/timeline.css +201 -0
- data/docs/_includes/edit-on-github.html +11 -0
- data/docs/_includes/example.html +21 -0
- data/docs/_includes/footer.html +49 -0
- data/docs/_includes/head.html +32 -0
- data/docs/_includes/header.html +15 -0
- data/docs/_includes/js.html +28 -0
- data/docs/_includes/js_disqus.html +21 -0
- data/docs/_includes/modals.html +40 -0
- data/docs/_includes/nav.html +27 -0
- data/docs/_includes/quotes.html +19 -0
- data/docs/_includes/subnav.html +35 -0
- data/docs/_includes/ufo-ship-options.md +13 -0
- data/docs/_includes/uses.html +19 -0
- data/docs/_layouts/default.html +11 -0
- data/docs/_layouts/style.css +6 -0
- data/docs/articles.md +5 -0
- data/docs/css/font-awesome/css/font-awesome.css +1566 -0
- data/docs/css/font-awesome/css/font-awesome.min.css +4 -0
- data/docs/css/font-awesome/fonts/FontAwesome.otf +0 -0
- data/docs/css/font-awesome/fonts/fontawesome-webfont.eot +0 -0
- data/docs/css/font-awesome/fonts/fontawesome-webfont.svg +504 -0
- data/docs/css/font-awesome/fonts/fontawesome-webfont.ttf +0 -0
- data/docs/css/font-awesome/fonts/fontawesome-webfont.woff +0 -0
- data/docs/docs.md +21 -0
- data/docs/img/logos/boltops-logo-full.png +0 -0
- data/docs/img/logos/boltops-logo.png +0 -0
- data/docs/img/sonic-screwdriver.jpg +0 -0
- data/docs/img/tutorials/ec2-console-public-ip.png +0 -0
- data/docs/img/ufo.jpg +0 -0
- data/docs/index.html +9 -0
- data/docs/js/bootstrap.js +2114 -0
- data/docs/js/bootstrap.min.js +6 -0
- data/docs/js/cbpAnimatedHeader.js +44 -0
- data/docs/js/cbpAnimatedHeader.min.js +11 -0
- data/docs/js/classie.js +80 -0
- data/docs/js/contact_me.js +70 -0
- data/docs/js/contact_me_static.js +23 -0
- data/docs/js/freelancer.js +37 -0
- data/docs/js/jqBootstrapValidation.js +912 -0
- data/docs/js/jquery-1.11.0.js +4 -0
- data/docs/js/jquery.easing.min.js +44 -0
- data/docs/js/nav.js +53 -0
- data/docs/quick-start.md +39 -0
- data/docs/style.css +3 -0
- data/lib/bash_scripts/docker-exec.sh +15 -0
- data/lib/bash_scripts/docker-run.sh +15 -0
- data/lib/sonic.rb +11 -2
- data/lib/sonic/aws_services.rb +19 -0
- data/lib/sonic/cli.rb +37 -8
- data/lib/sonic/cli/help.rb +123 -3
- data/lib/sonic/default/settings.yml +12 -0
- data/lib/sonic/docker.rb +128 -0
- data/lib/sonic/execute.rb +131 -0
- data/lib/sonic/list.rb +85 -0
- data/lib/sonic/settings.rb +80 -0
- data/lib/sonic/ssh.rb +136 -0
- data/lib/sonic/ssh/ec2_tag.rb +59 -0
- data/lib/sonic/ssh/identifier_detector.rb +145 -0
- data/lib/sonic/ui.rb +26 -0
- data/lib/sonic/version.rb +2 -2
- data/qa.md +21 -0
- data/sonic.gemspec +3 -1
- data/spec/fixtures/home/.gitkeep +0 -0
- data/spec/fixtures/project/.gitkeep +0 -0
- data/spec/fixtures/project/command.txt +2 -0
- data/spec/lib/cli_spec.rb +16 -6
- data/spec/lib/sonic/execute_spec.rb +35 -0
- data/spec/spec_helper.rb +5 -3
- metadata +133 -3
@@ -0,0 +1,131 @@
|
|
1
|
+
module Sonic
|
2
|
+
class Execute
|
3
|
+
include AwsServices
|
4
|
+
|
5
|
+
def initialize(command, options)
|
6
|
+
@command = command
|
7
|
+
@options = options
|
8
|
+
@filter = @options[:filter].split(',').map{|s| s.strip}
|
9
|
+
end
|
10
|
+
|
11
|
+
# aws ssm send-command \
|
12
|
+
# --instance-ids i-030033c20c54bf149 \
|
13
|
+
# --document-name "AWS-RunShellScript" \
|
14
|
+
# --comment "Demo run shell script on Linux Instances" \
|
15
|
+
# --parameters '{"commands":["#!/usr/bin/python","print \"Hello world from python\""]}' \
|
16
|
+
# --query "Command.CommandId"
|
17
|
+
def execute
|
18
|
+
ssm_options = build_ssm_options
|
19
|
+
if @options[:noop]
|
20
|
+
UI.noop = true
|
21
|
+
command_id = "fake command id"
|
22
|
+
else
|
23
|
+
resp = ssm.send_command(ssm_options)
|
24
|
+
command_id = resp.command.command_id
|
25
|
+
end
|
26
|
+
UI.say "Command sent to AWS SSM. To check the details of the command:"
|
27
|
+
list_command = "aws ssm list-commands --command-id #{command_id}"
|
28
|
+
UI.say list_command
|
29
|
+
if RUBY_PLATFORM =~ /darwin/
|
30
|
+
system("echo '#{list_command}' | pbcopy")
|
31
|
+
UI.say "Pro tip: the aws ssm command is already in your copy/paste clipboard."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_ssm_options
|
36
|
+
criteria = transform_filter(@filter)
|
37
|
+
command = build_command(@command)
|
38
|
+
criteria.merge(
|
39
|
+
document_name: "AWS-RunShellScript",
|
40
|
+
comment: "sonic #{ARGV.join(' ')}",
|
41
|
+
parameters: {
|
42
|
+
"commands" => command
|
43
|
+
}
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_command(command)
|
48
|
+
if file_path?(command)
|
49
|
+
path = file_path(command)
|
50
|
+
if File.exist?(path)
|
51
|
+
IO.readlines(path).map {|s| s.strip}
|
52
|
+
else
|
53
|
+
UI.error("File #{path} could not be found. Are you sure it exist?")
|
54
|
+
exit 1
|
55
|
+
end
|
56
|
+
else
|
57
|
+
# The script is being feed inline so just join the command together into one script.
|
58
|
+
# Still keep in an array form because that's how ssn.send_command with AWS-RunShellScript
|
59
|
+
# usually reads the command.
|
60
|
+
[command.join(" ")]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def file_path?(command)
|
65
|
+
return false unless command.size == 1
|
66
|
+
possible_path = command.first
|
67
|
+
possible_path.include?("file://")
|
68
|
+
end
|
69
|
+
|
70
|
+
def file_path(command)
|
71
|
+
path = command.first
|
72
|
+
path = path.sub('file://', '')
|
73
|
+
path = "#{@options[:project_root]}/#{path}" if @options[:project_root]
|
74
|
+
path
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Public: Transform the filter to the ssm send_command equivalent options
|
79
|
+
#
|
80
|
+
# filter - CLI filter option. Example: hi-web-prod hi-worker-prod hi-clock-prod i-0f7f833131a51ce35
|
81
|
+
#
|
82
|
+
# Examples
|
83
|
+
#
|
84
|
+
# transform_filter(["hi-web-prod", "hi-worker-prod", "i-006a097bb10643e20"])
|
85
|
+
# # => {
|
86
|
+
# instance_ids: ["i-006a097bb10643e20"],
|
87
|
+
# targets: [{key: "Name", values: "hi-web-prod,hi-worker-prod"}]
|
88
|
+
# }
|
89
|
+
#
|
90
|
+
# Returns the duplicated String.
|
91
|
+
def transform_filter(filter)
|
92
|
+
valid = validate_filter(filter)
|
93
|
+
unless valid
|
94
|
+
UI.error("The filter you provided '#{filter.join(',')}' is not valid.")
|
95
|
+
UI.say("The filter must either be all instance ids or just a list of tag names.")
|
96
|
+
exit 1
|
97
|
+
end
|
98
|
+
|
99
|
+
if filter.detect { |i| instance_id?(i) }
|
100
|
+
instance_ids = filter
|
101
|
+
{instance_ids: instance_ids}
|
102
|
+
else
|
103
|
+
tags = filter
|
104
|
+
targets = [{
|
105
|
+
key: "tag:#{tag_name}",
|
106
|
+
values: tags
|
107
|
+
}]
|
108
|
+
{targets: targets}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Either all instance ids are no instance ids is a valid filter
|
113
|
+
def validate_filter(filter)
|
114
|
+
if filter.detect { |i| instance_id?(i) }
|
115
|
+
instance_ids = filter.select { |i| instance_id?(i) }
|
116
|
+
instance_ids.size == filter.size
|
117
|
+
else
|
118
|
+
true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# TODO: make configurable
|
123
|
+
def tag_name
|
124
|
+
"Name"
|
125
|
+
end
|
126
|
+
|
127
|
+
def instance_id?(text)
|
128
|
+
text =~ /i-.{17}/
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
data/lib/sonic/list.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Sonic
|
2
|
+
class List
|
3
|
+
include AwsServices
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
@filter = @options[:filter] ? @options[:filter].split(',').map{|s| s.strip} : []
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
options = transform_filter_option(@filter)
|
12
|
+
if @options[:noop]
|
13
|
+
instances = []
|
14
|
+
else
|
15
|
+
instances = ec2_resource.instances(options)
|
16
|
+
end
|
17
|
+
display(instances)
|
18
|
+
end
|
19
|
+
|
20
|
+
def display(instances)
|
21
|
+
if @options[:header]
|
22
|
+
UI.say "Instance Id\tPublic IP\tPrivate IP\tType".colorize(:green)
|
23
|
+
end
|
24
|
+
|
25
|
+
instances.each do |i|
|
26
|
+
line = [i.instance_id, i.public_ip_address, i.private_ip_address, i.instance_type].join("\t")
|
27
|
+
UI.say(line)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Public: Transform the filter to the ssm send_command equivalent options
|
33
|
+
#
|
34
|
+
# filter - CLI filter option. Example: hi-web-prod hi-worker-prod hi-clock-prod i-0f7f833131a51ce35
|
35
|
+
#
|
36
|
+
# Examples
|
37
|
+
#
|
38
|
+
# transform_filter(["hi-web-prod", "hi-worker-prod", "i-006a097bb10643e20"])
|
39
|
+
# # => {
|
40
|
+
# instance_ids: ["i-006a097bb10643e20"],
|
41
|
+
# targets: [{key: "Name", values: "hi-web-prod,hi-worker-prod"}]
|
42
|
+
# }
|
43
|
+
#
|
44
|
+
# Note: method looks close to the Execute#transform_filter method but the criteria
|
45
|
+
# structure is slightly different.
|
46
|
+
#
|
47
|
+
# Returns the duplicated String.
|
48
|
+
def transform_filter_option(filter)
|
49
|
+
return {} if filter.empty?
|
50
|
+
|
51
|
+
valid = validate_filter(filter)
|
52
|
+
unless valid
|
53
|
+
UI.error("The filter you provided '#{filter.join(',')}' is not valid.")
|
54
|
+
UI.say("The filter must either be all instance ids or just a list of tag names.")
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
if filter.detect { |i| instance_id?(i) }
|
59
|
+
instance_ids = filter
|
60
|
+
{instance_ids: instance_ids}
|
61
|
+
else
|
62
|
+
tags = filter
|
63
|
+
criteria = [{
|
64
|
+
name: "tag-value",
|
65
|
+
values: tags
|
66
|
+
}]
|
67
|
+
{filters: criteria}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Either all instance ids are no instance ids is a valid filter
|
72
|
+
def validate_filter(filter)
|
73
|
+
if filter.detect { |i| instance_id?(i) }
|
74
|
+
instance_ids = filter.select { |i| instance_id?(i) }
|
75
|
+
instance_ids.size == filter.size
|
76
|
+
else
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def instance_id?(text)
|
82
|
+
text =~ /i-.{17}/
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Sonic
|
4
|
+
class Settings
|
5
|
+
def initialize(project_root=nil)
|
6
|
+
@project_root = project_root || '.'
|
7
|
+
end
|
8
|
+
|
9
|
+
def data
|
10
|
+
return @data if @data
|
11
|
+
|
12
|
+
project_file = "#{@project_root}/.sonic/settings.yml"
|
13
|
+
project = File.exist?(project_file) ? YAML.load_file(project_file) : {}
|
14
|
+
|
15
|
+
user_file = "#{home}/.sonic/settings.yml"
|
16
|
+
user = File.exist?(user_file) ? YAML.load_file(user_file) : {}
|
17
|
+
|
18
|
+
default_file = File.expand_path("../default/settings.yml", __FILE__)
|
19
|
+
default = YAML.load_file(default_file)
|
20
|
+
|
21
|
+
@data = default.merge(user.merge(project))
|
22
|
+
ensure_default_cluster(@data)
|
23
|
+
@data
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Returns default cluster based on the ECS service name.
|
27
|
+
#
|
28
|
+
# service - ECS service
|
29
|
+
# count - The Integer number of times to duplicate the text.
|
30
|
+
#
|
31
|
+
# The settings.yml format:
|
32
|
+
#
|
33
|
+
# service_cluster:
|
34
|
+
# default: stag
|
35
|
+
# hi-web-prod: prod
|
36
|
+
# hi-clock-prod: prod
|
37
|
+
# hi-worker-prod: prod
|
38
|
+
# hi-web-stag: stag
|
39
|
+
# hi-clock-stag: stag
|
40
|
+
# hi-worker-stag: stag
|
41
|
+
#
|
42
|
+
# Examples
|
43
|
+
#
|
44
|
+
# default_cluster('hi-web-prod')
|
45
|
+
# # => 'prod'
|
46
|
+
# default_cluster('whatever')
|
47
|
+
# # => 'stag'
|
48
|
+
#
|
49
|
+
# Returns the ECS cluster name.
|
50
|
+
def default_cluster(service)
|
51
|
+
service_cluster = data["service_cluster"]
|
52
|
+
service_cluster[service] || service_cluster["default"]
|
53
|
+
end
|
54
|
+
|
55
|
+
# When user's .sonic/settings.yml lack the default cluster, we add it on.
|
56
|
+
# Otherwise the user get confusing and scary aws-sdk-core/param_validator errors:
|
57
|
+
# Example: https://gist.github.com/sonic/67b9a68a77363b908d1c36047bc2709a
|
58
|
+
def ensure_default_cluster(data)
|
59
|
+
unless data["service_cluster"]["default"]
|
60
|
+
data["service_cluster"]["default"] = "default"
|
61
|
+
end
|
62
|
+
data
|
63
|
+
end
|
64
|
+
|
65
|
+
def host_key_check_options
|
66
|
+
if data["host_key_check"]
|
67
|
+
# no options by default enables strict host key checking
|
68
|
+
[]
|
69
|
+
else
|
70
|
+
# disables host key checking
|
71
|
+
%w[-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def home
|
76
|
+
# hack but fast
|
77
|
+
ENV['TEST'] ? "spec/fixtures/home" : ENV['HOME']
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/sonic/ssh.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Sonic
|
4
|
+
class Ssh
|
5
|
+
autoload :IdentifierDetector, 'sonic/ssh/identifier_detector'
|
6
|
+
|
7
|
+
include AwsServices
|
8
|
+
|
9
|
+
def initialize(identifier, options)
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
@user, @identifier = extract_user!(identifier) # extracts/strips user from identifier
|
13
|
+
# While --user option is supported at the class level, don't expose at the CLI level
|
14
|
+
# to encourage users to use user@host notation.
|
15
|
+
@user ||= options[:user] || settings.data["user"]
|
16
|
+
|
17
|
+
@service = @identifier # always set service even though it's not always used as the identifier
|
18
|
+
@cluster = options[:cluster] || settings.default_cluster(@service)
|
19
|
+
@bastion = options[:bastion] || settings.data["bastion"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
ssh = build_ssh_command
|
24
|
+
kernel_exec(*ssh) # must splat the Array here
|
25
|
+
end
|
26
|
+
|
27
|
+
def bastion_host
|
28
|
+
return @identifier if @options[:noop] # for specs
|
29
|
+
@bastion_host ||= build_bastion_host
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_bastion_host
|
33
|
+
host = @bastion
|
34
|
+
host = "#{@user}@#{host}" unless host.include?('@')
|
35
|
+
host
|
36
|
+
end
|
37
|
+
|
38
|
+
# used by child Classes
|
39
|
+
def ssh_host
|
40
|
+
return @identifier if @options[:noop] # for specs
|
41
|
+
@ssh_host ||= build_ssh_host
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_ssh_host
|
45
|
+
detector = Ssh::IdentifierDetector.new(@cluster, @service, @identifier, @options)
|
46
|
+
instance_id = detector.detect!
|
47
|
+
instance_hostname(instance_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def instance_hostname(ec2_instance_id)
|
51
|
+
begin
|
52
|
+
resp = ec2.describe_instances(instance_ids: [ec2_instance_id])
|
53
|
+
rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e
|
54
|
+
# e.message: The instance ID 'i-027363802c6ff3141' does not exist
|
55
|
+
UI.error(e.message)
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
instance = resp.reservations[0].instances[0]
|
59
|
+
# struct Aws::EC2::Types::Instance
|
60
|
+
# http://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/Types/Instance.html
|
61
|
+
host = if @bastion
|
62
|
+
instance.private_ip_address
|
63
|
+
else
|
64
|
+
instance.public_ip_address
|
65
|
+
end
|
66
|
+
"#{@user}@#{host}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Will use Kernel.exec so that the ssh process takes over this ruby process.
|
70
|
+
def kernel_exec(*args)
|
71
|
+
# append the optional command that can be provided to the ssh command
|
72
|
+
full_command = args + @options[:command]
|
73
|
+
puts "=> #{full_command.join(' ')}".colorize(:green)
|
74
|
+
# https://ruby-doc.org/core-2.3.1/Kernel.html#method-i-exec
|
75
|
+
# Using 2nd form
|
76
|
+
Kernel.exec(*full_command) unless @options[:noop]
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def settings
|
81
|
+
@settings ||= Settings.new(@options[:project_root])
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns Array of flags.
|
85
|
+
def ssh_options
|
86
|
+
settings.host_key_check_options
|
87
|
+
end
|
88
|
+
|
89
|
+
# Will prepend the bastion host if required
|
90
|
+
# When bastion set
|
91
|
+
# ssh [options] -At [bastion_host] ssh -At [ssh_host]
|
92
|
+
#
|
93
|
+
# When bastion not set
|
94
|
+
# ssh [options] -At [ssh_host]
|
95
|
+
#
|
96
|
+
# Builds up ssh command to be used with Kernel.exec. Will look something like this:
|
97
|
+
# ssh -At ec2-user@34.211.223.3 ssh ec2-user@10.10.110.135
|
98
|
+
# It is imporant to use an Array for the command so it gets intrepreted as if you are
|
99
|
+
# executing it from the shell directly. For example, globs gets expanded with the
|
100
|
+
# Array notation but not the String notation.
|
101
|
+
#
|
102
|
+
# ssh options:
|
103
|
+
# -A = Enables forwarding of the authentication agent connection
|
104
|
+
# -t = Force pseudo-terminal allocati
|
105
|
+
def build_ssh_command
|
106
|
+
ssh = ["ssh"] + ssh_options
|
107
|
+
ssh += ["-At", bastion_host, "ssh"] if @bastion
|
108
|
+
# ssh_host is internal ip when bastion is set
|
109
|
+
# ssh_host is public ip when bastion is not set
|
110
|
+
ssh += ["-At", ssh_host]
|
111
|
+
ssh
|
112
|
+
end
|
113
|
+
|
114
|
+
# Private: Extracts and strips the user from the identifier.
|
115
|
+
#
|
116
|
+
# identifier - Can be a variety of things: instance_id, ecs service, ecs task, etc.
|
117
|
+
#
|
118
|
+
# Examples
|
119
|
+
#
|
120
|
+
# extract_user!("i-0f7f833131a51ce35")
|
121
|
+
# # => [nil, "i-0f7f833131a51ce35"]
|
122
|
+
#
|
123
|
+
# extract_user!("ec2-user@i-0f7f833131a51ce35")
|
124
|
+
# # => ["ec2-user", "i-0f7f833131a51ce35"]
|
125
|
+
#
|
126
|
+
# Returns the a tuple cotaining the user and identifier
|
127
|
+
def extract_user!(identifier)
|
128
|
+
md = identifier.match(/(.*)@(.*)/)
|
129
|
+
if md
|
130
|
+
[md[1], md[2]]
|
131
|
+
else
|
132
|
+
[nil, identifier]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'tty-prompt'
|
2
|
+
|
3
|
+
module Sonic
|
4
|
+
# ec2 tag related methods
|
5
|
+
module Ssh::Ec2Tag
|
6
|
+
def ec2_instances
|
7
|
+
return @ec2_instances if @ec2_instances
|
8
|
+
|
9
|
+
filters = [{ name: 'tag-value', values: tags_filter }]
|
10
|
+
@ec2_instances = ec2_resource.instances(filters: filters)
|
11
|
+
end
|
12
|
+
|
13
|
+
# matches any tag value
|
14
|
+
def ec2_tag_exists?
|
15
|
+
ec2_instances.count > 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# If no instances found
|
19
|
+
# Exit immediately with error message
|
20
|
+
# If all instances found have the same tag name
|
21
|
+
# Immediately return the first instance id
|
22
|
+
# If multiple tag values
|
23
|
+
# Prompt user to select instance tag value of interest
|
24
|
+
def find_ec2_instance
|
25
|
+
tag_values = ec2_instances.map{ |i| matched_tag_value(i) }.uniq
|
26
|
+
case tag_values.size
|
27
|
+
when 0
|
28
|
+
UI.error("Unable to find an instance with a one of the tag values: #{@identifier}")
|
29
|
+
exit 1
|
30
|
+
when 1
|
31
|
+
ec2_instances.first.instance_id
|
32
|
+
else
|
33
|
+
# prompt
|
34
|
+
select_instance_type(tag_values).instance_id
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_instance_type(tag_values)
|
39
|
+
UI.say("Found multiple instance types matching the tag filter: #{@identifier}")
|
40
|
+
prompt = TTY::Prompt.new
|
41
|
+
tag_value = prompt.select("Select an instance type tag:", tag_values)
|
42
|
+
|
43
|
+
# find the first instance with the tag_value
|
44
|
+
instance = ec2_instances.find do |i|
|
45
|
+
i.tags.find { |t| t.value == tag_value }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def matched_tag_value(instance)
|
50
|
+
tags = instance.tags
|
51
|
+
tag = tags.find {|t| tags_filter.include?(t.value) }
|
52
|
+
tag.value
|
53
|
+
end
|
54
|
+
|
55
|
+
def tags_filter
|
56
|
+
@identifier.split(',') # identifier from CLI could be a comma separated list
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|