mtncloud-cli 1.0.0 → 1.0.2

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.
@@ -25,7 +25,7 @@ class MtnCloud::Cli::Login
25
25
  username, password = nil, nil
26
26
  optparse = MtnCloud::Cli::OptionParser.new do |opts|
27
27
  opts.banner = usage
28
- opts.on( '-u', '--username USERNAME', "Username. Sub-tenant users must format their username with a prefix like {subdomain}\\{username}" ) do |val|
28
+ opts.on( '-u', '--username USERNAME', "Username. Sub-tenant users prefix it with their subdomain or account ID: {subdomain}\\{username} or {accountID}\\{username}" ) do |val|
29
29
  username = val
30
30
  end
31
31
  opts.on( '-p', '--password PASSWORD', "Password" ) do |val|
@@ -54,8 +54,13 @@ Login to a remote appliance with a username and password or using an access toke
54
54
  Logging in with username and password will make an authentication api request to obtain an access token.
55
55
  [username] is required, this is the username of the MTN Cloud user
56
56
  [password] is required, this is the password of the MTN Cloud user
57
- Sub-tenant users will need to pass their tenant subdomain prefix. ie. {subdomain}\\{username}
58
- By default, the subdomain is the tenant account ID. Example: 2\\neo
57
+ Username format:
58
+ Master tenant users enter only their username. Example: neo
59
+ Sub-tenant users prefix their username with their tenant's subdomain or account ID:
60
+ {subdomain}\\{username} Example: acme\\neo
61
+ {accountID}\\{username} Example: 2\\neo
62
+ A tenant's subdomain is its account ID unless a custom subdomain is set.
63
+ In bash or zsh, quote a username that contains a backslash: '2\\neo'
59
64
  The --token option can be used to login with a valid access token instead of username and password.
60
65
  The specified token will be verified by making a whoami api request
61
66
  If successful, the access token will be saved with the active session for the remote appliance.
@@ -11,6 +11,10 @@ module MtnCloud
11
11
  class Credentials
12
12
  include MtnCloud::Cli::PrintHelper
13
13
 
14
+ # How sub-tenant users format their username, shown in the login prompt.
15
+ # login --help explains the same formats in more detail.
16
+ USERNAME_FORMAT = "sub-tenant users: subdomain\\username or accountID\\username"
17
+
14
18
  @@appliance_credentials_map = nil
15
19
 
16
20
  def initialize(appliance_name, appliance_url)
@@ -118,7 +122,7 @@ module MtnCloud
118
122
  Readline.basic_word_break_characters = ''
119
123
  Readline.completer_word_break_characters = '' rescue nil
120
124
  Readline.completion_proc = nil
121
- username = Readline.readline("Username: #{required_blue_prompt} ", false).to_s.chomp
125
+ username = Readline.readline("Username (#{USERNAME_FORMAT}): #{required_blue_prompt} ", false).to_s.chomp
122
126
  else
123
127
  print "Username: #{required_blue_prompt} #{username}\n"
124
128
  end
@@ -1,6 +1,6 @@
1
1
 
2
2
  module MtnCloud
3
3
  module Cli
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
5
  end
6
6
  end
@@ -1,109 +1,109 @@
1
- # Provide deep_merge.
2
- # Borrowed from rails active_support
3
- # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
4
- #
5
- class Hash
6
- # Returns a new hash with +self+ and +other_hash+ merged recursively.
7
- #
8
- # h1 = { a: true, b: { c: [1, 2, 3] } }
9
- # h2 = { a: false, b: { x: [3, 4, 5] } }
10
- #
11
- # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
12
- #
13
- # Like with Hash#merge in the standard library, a block can be provided
14
- # to merge values:
15
- #
16
- # h1 = { a: 100, b: 200, c: { c1: 100 } }
17
- # h2 = { b: 250, c: { c1: 200 } }
18
- # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
19
- # # => { a: 100, b: 450, c: { c1: 300 } }
20
- def deep_merge(other_hash, &block)
21
- dup.deep_merge!(other_hash, &block)
22
- end
23
-
24
- # Same as +deep_merge+, but modifies +self+.
25
- def deep_merge!(other_hash, &block)
26
- other_hash.each_pair do |current_key, other_value|
27
- this_value = self[current_key]
28
-
29
- self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
30
- this_value.deep_merge(other_value, &block)
31
- else
32
- if block_given? && key?(current_key)
33
- block.call(current_key, this_value, other_value)
34
- else
35
- other_value
36
- end
37
- end
38
- end
39
-
40
- self
41
- end
42
-
43
- def deep_compact!
44
- self.each_pair do |k, v|
45
- if v.is_a?(Hash)
46
- self[k].deep_compact!
47
- elsif v.is_a?(Array)
48
- self[k].each do |it|
49
- if it.is_a?(Hash)
50
- it.deep_compact!
51
- elsif self[k] == nil || self[k] == ''
52
- # meh, preserve 'empty' array elements
53
- end
54
- end
55
- else
56
- if self[k] == nil || self[k] == ''
57
- self.delete(k)
58
- end
59
- end
60
- end
61
- self
62
- end
63
-
64
- # convert recognizable strings to booleans
65
- def booleanize!(true_values=['true','on'], false_values=['false','off'])
66
- self.each_pair do |k, v|
67
- if v.is_a?(Hash)
68
- self[k].booleanize!
69
- elsif v.is_a?(Array)
70
- self[k].each do |it|
71
- if it.is_a?(Hash)
72
- it.booleanize!
73
- elsif self[k] == nil || self[k] == ''
74
- # meh, preserve 'empty' array elements
75
- end
76
- end
77
- else
78
- if true_values.include?(v)
79
- self[k] = true
80
- elsif false_values.include?(v)
81
- self[k] = false
82
- end
83
- end
84
- end
85
- self
86
- end
87
-
88
- def upcase_keys!
89
- self.keys.each do |k|
90
- self[k.to_s.upcase] = self.delete(k)
91
- end
92
- self
93
- end
94
-
95
- def downcase_keys!
96
- self.keys.each do |k|
97
- self[k.to_s.downcase] = self.delete(k)
98
- end
99
- self
100
- end
101
-
102
- def capitalize_keys!
103
- self.keys.each do |k|
104
- self[k.to_s.capitalize] = self.delete(k)
105
- end
106
- self
107
- end
108
-
109
- end
1
+ # Provide deep_merge.
2
+ # Borrowed from rails active_support
3
+ # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
4
+ #
5
+ class Hash
6
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
7
+ #
8
+ # h1 = { a: true, b: { c: [1, 2, 3] } }
9
+ # h2 = { a: false, b: { x: [3, 4, 5] } }
10
+ #
11
+ # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
12
+ #
13
+ # Like with Hash#merge in the standard library, a block can be provided
14
+ # to merge values:
15
+ #
16
+ # h1 = { a: 100, b: 200, c: { c1: 100 } }
17
+ # h2 = { b: 250, c: { c1: 200 } }
18
+ # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
19
+ # # => { a: 100, b: 450, c: { c1: 300 } }
20
+ def deep_merge(other_hash, &block)
21
+ dup.deep_merge!(other_hash, &block)
22
+ end
23
+
24
+ # Same as +deep_merge+, but modifies +self+.
25
+ def deep_merge!(other_hash, &block)
26
+ other_hash.each_pair do |current_key, other_value|
27
+ this_value = self[current_key]
28
+
29
+ self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
30
+ this_value.deep_merge(other_value, &block)
31
+ else
32
+ if block_given? && key?(current_key)
33
+ block.call(current_key, this_value, other_value)
34
+ else
35
+ other_value
36
+ end
37
+ end
38
+ end
39
+
40
+ self
41
+ end
42
+
43
+ def deep_compact!
44
+ self.each_pair do |k, v|
45
+ if v.is_a?(Hash)
46
+ self[k].deep_compact!
47
+ elsif v.is_a?(Array)
48
+ self[k].each do |it|
49
+ if it.is_a?(Hash)
50
+ it.deep_compact!
51
+ elsif self[k] == nil || self[k] == ''
52
+ # meh, preserve 'empty' array elements
53
+ end
54
+ end
55
+ else
56
+ if self[k] == nil || self[k] == ''
57
+ self.delete(k)
58
+ end
59
+ end
60
+ end
61
+ self
62
+ end
63
+
64
+ # convert recognizable strings to booleans
65
+ def booleanize!(true_values=['true','on'], false_values=['false','off'])
66
+ self.each_pair do |k, v|
67
+ if v.is_a?(Hash)
68
+ self[k].booleanize!
69
+ elsif v.is_a?(Array)
70
+ self[k].each do |it|
71
+ if it.is_a?(Hash)
72
+ it.booleanize!
73
+ elsif self[k] == nil || self[k] == ''
74
+ # meh, preserve 'empty' array elements
75
+ end
76
+ end
77
+ else
78
+ if true_values.include?(v)
79
+ self[k] = true
80
+ elsif false_values.include?(v)
81
+ self[k] = false
82
+ end
83
+ end
84
+ end
85
+ self
86
+ end
87
+
88
+ def upcase_keys!
89
+ self.keys.each do |k|
90
+ self[k.to_s.upcase] = self.delete(k)
91
+ end
92
+ self
93
+ end
94
+
95
+ def downcase_keys!
96
+ self.keys.each do |k|
97
+ self[k.to_s.downcase] = self.delete(k)
98
+ end
99
+ self
100
+ end
101
+
102
+ def capitalize_keys!
103
+ self.keys.each do |k|
104
+ self[k.to_s.capitalize] = self.delete(k)
105
+ end
106
+ self
107
+ end
108
+
109
+ end
@@ -1,5 +1,5 @@
1
- class NilClass
2
- def empty?
3
- true
4
- end
5
- end
1
+ class NilClass
2
+ def empty?
3
+ true
4
+ end
5
+ end
@@ -1,37 +1,37 @@
1
- # this does some monkey patching of third party RestClient
2
- # to modify the logging output a bit
3
- require 'rest-client'
4
-
5
- module RestClient
6
- class Request
7
-
8
- alias_method :original_log_request, :log_request
9
- def log_request
10
- begin
11
- return unless RestClient.log
12
- out = []
13
- # out << "RestClient.#{method} #{redacted_url.inspect}"
14
- out << "#{method.to_s.upcase} #{redacted_url.inspect}"
15
- out << payload.short_inspect if payload
16
- out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
17
- RestClient.log << out.join(', ') + "\n"
18
- rescue
19
- # something went wrong, wrong gem version maybe...above is from rest-client 2.0.2
20
- # do it the old way
21
- original_log_request()
22
- end
23
- end
24
- =begin
25
- def log_response res
26
- return unless RestClient.log
27
- size = if @raw_response
28
- File.size(@tf.path)
29
- else
30
- res.body.nil? ? 0 : res.body.size
31
- end
32
-
33
- RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
34
- end
35
- =end
36
- end
37
- end
1
+ # this does some monkey patching of third party RestClient
2
+ # to modify the logging output a bit
3
+ require 'rest-client'
4
+
5
+ module RestClient
6
+ class Request
7
+
8
+ alias_method :original_log_request, :log_request
9
+ def log_request
10
+ begin
11
+ return unless RestClient.log
12
+ out = []
13
+ # out << "RestClient.#{method} #{redacted_url.inspect}"
14
+ out << "#{method.to_s.upcase} #{redacted_url.inspect}"
15
+ out << payload.short_inspect if payload
16
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
17
+ RestClient.log << out.join(', ') + "\n"
18
+ rescue
19
+ # something went wrong, wrong gem version maybe...above is from rest-client 2.0.2
20
+ # do it the old way
21
+ original_log_request()
22
+ end
23
+ end
24
+ =begin
25
+ def log_response res
26
+ return unless RestClient.log
27
+ size = if @raw_response
28
+ File.size(@tf.path)
29
+ else
30
+ res.body.nil? ? 0 : res.body.size
31
+ end
32
+
33
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
34
+ end
35
+ =end
36
+ end
37
+ end
@@ -1,66 +1,66 @@
1
- class String
2
-
3
- def pluralize
4
- # hacky version of this until we want to depend on ActiveSupport
5
- value = self.dup
6
- if value == ""
7
- value
8
- elsif value[-1].chr == "y"
9
- value[0..-2] + "ies"
10
- elsif value[-1].chr == "s"
11
- if value[-2..-1] == "ss"
12
- value + "es"
13
- else
14
- value
15
- end
16
- else
17
- value + "s"
18
- end
19
- end
20
-
21
- def singularize
22
- # hacky version of this until we want to depend on ActiveSupport
23
- value = self.dup
24
- if value == ""
25
- value
26
- elsif value.size > 3 && value[-3..-1] == "ies"
27
- value[0..-4] + "y"
28
- elsif value.size > 2 && value[-3..-1] == "ses"
29
- value[0..-2]
30
- elsif value[-1] == "s"
31
- value[0..-2]
32
- else
33
- value
34
- end
35
- end
36
-
37
- def underscore
38
- value = self.dup
39
- value.gsub!(/::/, '/')
40
- value.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
41
- value.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
42
- value.tr!("-", "_")
43
- value.tr!(" ", "_")
44
- value.downcase!
45
- value
46
- end
47
-
48
- def camelcase
49
- value = self.underscore.gsub(/\_([a-z])/) do $1.upcase end
50
- value = value[0, 1].downcase + value[1..-1]
51
- value
52
- end
53
-
54
- def upcamelcase
55
- self.camelcase.capitalize
56
- end
57
-
58
- def titleize
59
- self.underscore.split("_").map(&:capitalize).join(" ")
60
- end
61
-
62
- def dasherize
63
- self.underscore.gsub("_", "-")
64
- end
65
-
66
- end
1
+ class String
2
+
3
+ def pluralize
4
+ # hacky version of this until we want to depend on ActiveSupport
5
+ value = self.dup
6
+ if value == ""
7
+ value
8
+ elsif value[-1].chr == "y"
9
+ value[0..-2] + "ies"
10
+ elsif value[-1].chr == "s"
11
+ if value[-2..-1] == "ss"
12
+ value + "es"
13
+ else
14
+ value
15
+ end
16
+ else
17
+ value + "s"
18
+ end
19
+ end
20
+
21
+ def singularize
22
+ # hacky version of this until we want to depend on ActiveSupport
23
+ value = self.dup
24
+ if value == ""
25
+ value
26
+ elsif value.size > 3 && value[-3..-1] == "ies"
27
+ value[0..-4] + "y"
28
+ elsif value.size > 2 && value[-3..-1] == "ses"
29
+ value[0..-2]
30
+ elsif value[-1] == "s"
31
+ value[0..-2]
32
+ else
33
+ value
34
+ end
35
+ end
36
+
37
+ def underscore
38
+ value = self.dup
39
+ value.gsub!(/::/, '/')
40
+ value.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
41
+ value.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
42
+ value.tr!("-", "_")
43
+ value.tr!(" ", "_")
44
+ value.downcase!
45
+ value
46
+ end
47
+
48
+ def camelcase
49
+ value = self.underscore.gsub(/\_([a-z])/) do $1.upcase end
50
+ value = value[0, 1].downcase + value[1..-1]
51
+ value
52
+ end
53
+
54
+ def upcamelcase
55
+ self.camelcase.capitalize
56
+ end
57
+
58
+ def titleize
59
+ self.underscore.split("_").map(&:capitalize).join(" ")
60
+ end
61
+
62
+ def dasherize
63
+ self.underscore.gsub("_", "-")
64
+ end
65
+
66
+ end
data/mtncloud-cli.gemspec CHANGED
@@ -1,38 +1,38 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'mtncloud/cli/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "mtncloud-cli"
8
- spec.version = MtnCloud::Cli::VERSION
9
- spec.authors = ["Joshua Ukpozi", "Joseph Henry", "MTN Cloud"]
10
- spec.summary = "Provides CLI Interface to MTN Cloud"
11
- spec.description = "Cloud application management & orchestration CLI for MTN Cloud. Easily manage and orchestrate VMs on private or public infrastructure and containerized architectures."
12
- spec.license = "MIT"
13
-
14
- spec.files = `git ls-files -z`.split("\x0")
15
-
16
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
- spec.require_paths = ["lib"]
19
-
20
- spec.required_ruby_version = '>= 2.5.1' # according to http.rb doc
21
- spec.add_dependency "tins", '~> 1.43.0'
22
- spec.add_dependency "public_suffix", "4.0.7" # https://github.com/weppos/publicsuffix-ruby/blob/main/CHANGELOG.md
23
- spec.add_dependency "ffi", "1.15.5" # Fix for mismatched deps: https://github.com/ffi/ffi/issues/1103
24
- spec.add_development_dependency "bundler", ">= 2.2"
25
- spec.add_development_dependency "rake"
26
- spec.add_dependency 'term-ansicolor', '~> 1.3.0'
27
- spec.add_dependency "rest-client", "2.0.2"
28
- spec.add_dependency 'multipart-post'
29
- spec.add_dependency "filesize"
30
- spec.add_dependency 'mime-types'
31
- spec.add_dependency "http", "5.3.1"
32
- spec.add_dependency "rubyzip", '~> 2.3.2'
33
- spec.add_dependency "test-unit"
34
- # No longer part of Ruby's default libraries as of Ruby 4.0
35
- spec.add_dependency "readline"
36
- spec.add_dependency "logger"
37
- spec.add_dependency "ostruct"
38
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mtncloud/cli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mtncloud-cli"
8
+ spec.version = MtnCloud::Cli::VERSION
9
+ spec.authors = ["Joseph Henry", "Joshua Ukpozi", "MTN Cloud"]
10
+ spec.summary = "Provides CLI Interface to MTN Cloud"
11
+ spec.description = "Cloud application management & orchestration CLI for MTN Cloud. Easily manage and orchestrate VMs on private or public infrastructure and containerized architectures."
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.required_ruby_version = '>= 2.5.1' # according to http.rb doc
21
+ spec.add_dependency "tins", '~> 1.43.0'
22
+ spec.add_dependency "public_suffix", "4.0.7" # https://github.com/weppos/publicsuffix-ruby/blob/main/CHANGELOG.md
23
+ spec.add_dependency "ffi", "1.15.5" # Fix for mismatched deps: https://github.com/ffi/ffi/issues/1103
24
+ spec.add_development_dependency "bundler", ">= 2.2"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_dependency 'term-ansicolor', '~> 1.3.0'
27
+ spec.add_dependency "rest-client", "2.0.2"
28
+ spec.add_dependency 'multipart-post'
29
+ spec.add_dependency "filesize"
30
+ spec.add_dependency 'mime-types'
31
+ spec.add_dependency "http", "5.3.1"
32
+ spec.add_dependency "rubyzip", '~> 2.3.2'
33
+ spec.add_dependency "test-unit"
34
+ # No longer part of Ruby's default libraries as of Ruby 4.0
35
+ spec.add_dependency "readline"
36
+ spec.add_dependency "logger"
37
+ spec.add_dependency "ostruct"
38
+ end
@@ -0,0 +1,47 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'mtncloud'
4
+
5
+ # Offline tests for how login explains the username format.
6
+ # Sub-tenant users log in as {subdomain}\{username} or {accountID}\{username}.
7
+ class LoginHelpTest < Test::Unit::TestCase
8
+
9
+ def capture_stdout
10
+ saved = $stdout
11
+ $stdout = StringIO.new
12
+ yield
13
+ $stdout.string
14
+ ensure
15
+ $stdout = saved
16
+ end
17
+
18
+ # Captured once: the CLI keeps the stdout it first printed to, so a second
19
+ # --help in the same process would print to the first test's StringIO.
20
+ def login_help
21
+ @@login_help ||= capture_stdout do
22
+ error = assert_raise(SystemExit) { MtnCloud::Cli::Login.new.handle(['--help']) }
23
+ assert_equal 0, error.status
24
+ end
25
+ end
26
+
27
+ def test_help_documents_both_sub_tenant_username_formats
28
+ output = login_help
29
+ assert_include output, '{subdomain}\\{username}'
30
+ assert_include output, '{accountID}\\{username}'
31
+ assert_include output, 'Example: acme\\neo'
32
+ assert_include output, 'Example: 2\\neo'
33
+ assert_include output, 'Master tenant users enter only their username.'
34
+ end
35
+
36
+ def test_username_option_mentions_subdomain_and_account_id
37
+ output = login_help
38
+ assert_include output, '--username USERNAME'
39
+ assert_include output, 'Sub-tenant users prefix it with their subdomain or account ID'
40
+ end
41
+
42
+ def test_prompt_hint_names_both_formats
43
+ hint = MtnCloud::Cli::Credentials::USERNAME_FORMAT
44
+ assert_include hint, 'subdomain\\username'
45
+ assert_include hint, 'accountID\\username'
46
+ end
47
+ end