MuranoCLI 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.agignore +3 -0
  3. data/.gitignore +18 -1
  4. data/.rubocop.yml +222 -0
  5. data/.trustme.sh +185 -0
  6. data/.trustme.vim +24 -0
  7. data/Gemfile +23 -4
  8. data/LICENSE.txt +1 -1
  9. data/MuranoCLI.gemspec +43 -8
  10. data/README.markdown +9 -11
  11. data/Rakefile +187 -143
  12. data/TODO.taskpaper +2 -2
  13. data/bin/murano +51 -52
  14. data/docs/basic_example.rst +436 -0
  15. data/docs/completions/murano_completion-bash +3484 -0
  16. data/docs/demo.md +32 -32
  17. data/docs/develop.rst +391 -0
  18. data/lib/MrMurano.rb +21 -7
  19. data/lib/MrMurano/Account.rb +159 -174
  20. data/lib/MrMurano/Business.rb +381 -0
  21. data/lib/MrMurano/Config-Migrate.rb +32 -26
  22. data/lib/MrMurano/Config.rb +407 -128
  23. data/lib/MrMurano/Content.rb +191 -0
  24. data/lib/MrMurano/Gateway.rb +489 -0
  25. data/lib/MrMurano/Keystore.rb +48 -0
  26. data/lib/MrMurano/Passwords.rb +103 -0
  27. data/lib/MrMurano/ProjectFile.rb +121 -79
  28. data/lib/MrMurano/ReCommander.rb +114 -10
  29. data/lib/MrMurano/Setting.rb +90 -0
  30. data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
  31. data/lib/MrMurano/Solution-Services.rb +461 -166
  32. data/lib/MrMurano/Solution-Users.rb +70 -31
  33. data/lib/MrMurano/Solution.rb +372 -13
  34. data/lib/MrMurano/SolutionId.rb +73 -0
  35. data/lib/MrMurano/SyncRoot.rb +137 -0
  36. data/lib/MrMurano/SyncUpDown.rb +594 -284
  37. data/lib/MrMurano/Webservice-Cors.rb +71 -0
  38. data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
  39. data/lib/MrMurano/Webservice-File.rb +193 -0
  40. data/lib/MrMurano/Webservice.rb +51 -0
  41. data/lib/MrMurano/commands.rb +18 -15
  42. data/lib/MrMurano/commands/business.rb +300 -6
  43. data/lib/MrMurano/commands/completion-bash.erb +166 -0
  44. data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
  45. data/lib/MrMurano/commands/completion.rb +76 -39
  46. data/lib/MrMurano/commands/config.rb +108 -44
  47. data/lib/MrMurano/commands/content.rb +115 -72
  48. data/lib/MrMurano/commands/cors.rb +29 -14
  49. data/lib/MrMurano/commands/devices.rb +286 -0
  50. data/lib/MrMurano/commands/domain.rb +52 -12
  51. data/lib/MrMurano/commands/gb.rb +24 -9
  52. data/lib/MrMurano/commands/globals.rb +64 -0
  53. data/lib/MrMurano/commands/init.rb +377 -155
  54. data/lib/MrMurano/commands/keystore.rb +92 -82
  55. data/lib/MrMurano/commands/link.rb +300 -0
  56. data/lib/MrMurano/commands/login.rb +74 -11
  57. data/lib/MrMurano/commands/logs.rb +63 -32
  58. data/lib/MrMurano/commands/mock.rb +57 -29
  59. data/lib/MrMurano/commands/password.rb +57 -39
  60. data/lib/MrMurano/commands/postgresql.rb +127 -94
  61. data/lib/MrMurano/commands/settings.rb +203 -0
  62. data/lib/MrMurano/commands/show.rb +79 -38
  63. data/lib/MrMurano/commands/solution.rb +423 -5
  64. data/lib/MrMurano/commands/solution_picker.rb +547 -0
  65. data/lib/MrMurano/commands/status.rb +195 -61
  66. data/lib/MrMurano/commands/sync.rb +78 -39
  67. data/lib/MrMurano/commands/timeseries.rb +71 -55
  68. data/lib/MrMurano/commands/tsdb.rb +113 -87
  69. data/lib/MrMurano/commands/usage.rb +57 -15
  70. data/lib/MrMurano/hash.rb +100 -10
  71. data/lib/MrMurano/http.rb +187 -43
  72. data/lib/MrMurano/makePretty.rb +16 -14
  73. data/lib/MrMurano/optparse.rb +2178 -0
  74. data/lib/MrMurano/progress.rb +138 -0
  75. data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
  76. data/lib/MrMurano/template/projectFile.murano.erb +16 -13
  77. data/lib/MrMurano/verbosing.rb +166 -29
  78. data/lib/MrMurano/version.rb +30 -1
  79. data/spec/Account-Passwords_spec.rb +21 -4
  80. data/spec/Account_spec.rb +69 -146
  81. data/spec/Business_spec.rb +290 -0
  82. data/spec/ConfigFile_spec.rb +1 -0
  83. data/spec/ConfigMigrate_spec.rb +12 -8
  84. data/spec/Config_spec.rb +40 -34
  85. data/spec/Content_spec.rb +363 -0
  86. data/spec/GatewayBase_spec.rb +54 -0
  87. data/spec/GatewayDevice_spec.rb +321 -0
  88. data/spec/GatewayResource_spec.rb +266 -0
  89. data/spec/GatewaySettings_spec.rb +120 -0
  90. data/spec/Http_spec.rb +18 -8
  91. data/spec/Mock_spec.rb +2 -2
  92. data/spec/ProjectFile_spec.rb +25 -14
  93. data/spec/Setting_spec.rb +110 -0
  94. data/spec/Solution-ServiceConfig_spec.rb +44 -5
  95. data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
  96. data/spec/Solution-ServiceModules_spec.rb +47 -37
  97. data/spec/Solution-UsersRoles_spec.rb +10 -8
  98. data/spec/Solution_spec.rb +17 -8
  99. data/spec/SyncRoot_spec.rb +46 -20
  100. data/spec/SyncUpDown_spec.rb +437 -201
  101. data/spec/Verbosing_spec.rb +12 -4
  102. data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
  103. data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
  104. data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
  105. data/spec/Webservice-Setting_spec.rb +89 -0
  106. data/spec/_workspace.rb +4 -4
  107. data/spec/cmd_business_spec.rb +9 -4
  108. data/spec/cmd_common.rb +44 -1
  109. data/spec/cmd_content_spec.rb +43 -17
  110. data/spec/cmd_cors_spec.rb +4 -4
  111. data/spec/cmd_device_spec.rb +61 -16
  112. data/spec/cmd_domain_spec.rb +29 -6
  113. data/spec/cmd_init_spec.rb +281 -126
  114. data/spec/cmd_keystore_spec.rb +3 -3
  115. data/spec/cmd_link_spec.rb +98 -0
  116. data/spec/cmd_password_spec.rb +1 -1
  117. data/spec/cmd_setting_application_spec.rb +260 -0
  118. data/spec/cmd_setting_product_spec.rb +220 -0
  119. data/spec/cmd_status_spec.rb +223 -114
  120. data/spec/cmd_syncdown_spec.rb +115 -35
  121. data/spec/cmd_syncup_spec.rb +68 -15
  122. data/spec/cmd_usage_spec.rb +35 -8
  123. data/spec/fixtures/dumped_config +6 -4
  124. data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
  125. data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
  126. data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
  127. data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
  128. data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
  129. data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
  130. data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
  131. data/spec/spec_helper.rb +5 -0
  132. metadata +262 -171
  133. data/bin/mr +0 -8
  134. data/lib/MrMurano/Product-1P-Device.rb +0 -145
  135. data/lib/MrMurano/Product-Resources.rb +0 -205
  136. data/lib/MrMurano/Product.rb +0 -358
  137. data/lib/MrMurano/Solution-Cors.rb +0 -47
  138. data/lib/MrMurano/Solution-Endpoint.rb +0 -191
  139. data/lib/MrMurano/Solution-File.rb +0 -166
  140. data/lib/MrMurano/commands/assign.rb +0 -57
  141. data/lib/MrMurano/commands/businessList.rb +0 -45
  142. data/lib/MrMurano/commands/product.rb +0 -14
  143. data/lib/MrMurano/commands/productCreate.rb +0 -39
  144. data/lib/MrMurano/commands/productDelete.rb +0 -33
  145. data/lib/MrMurano/commands/productDevice.rb +0 -87
  146. data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
  147. data/lib/MrMurano/commands/productList.rb +0 -45
  148. data/lib/MrMurano/commands/productWrite.rb +0 -27
  149. data/lib/MrMurano/commands/solutionCreate.rb +0 -41
  150. data/lib/MrMurano/commands/solutionDelete.rb +0 -34
  151. data/lib/MrMurano/commands/solutionList.rb +0 -45
  152. data/spec/ProductBase_spec.rb +0 -113
  153. data/spec/ProductContent_spec.rb +0 -162
  154. data/spec/ProductResources_spec.rb +0 -329
  155. data/spec/Product_1P_Device_spec.rb +0 -202
  156. data/spec/Product_1P_RPC_spec.rb +0 -175
  157. data/spec/Product_spec.rb +0 -153
  158. data/spec/Solution-ServiceDevice_spec.rb +0 -176
  159. data/spec/cmd_assign_spec.rb +0 -51
@@ -1,3 +1,13 @@
1
+ # Last Modified: 2017.08.16 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
7
+
8
+ # 2017-07-01: This ordered list hacks around having
9
+ # individual files include all the files they need.
10
+
1
11
  require 'MrMurano/version'
2
12
  require 'MrMurano/verbosing'
3
13
  require 'MrMurano/hash'
@@ -7,15 +17,19 @@ require 'MrMurano/Config'
7
17
  require 'MrMurano/ProjectFile'
8
18
 
9
19
  require 'MrMurano/Account'
20
+ require 'MrMurano/Business'
21
+
22
+ require 'MrMurano/Content'
23
+ require 'MrMurano/Gateway'
24
+ require 'MrMurano/Setting'
10
25
  require 'MrMurano/Solution'
11
- require 'MrMurano/Solution-Endpoint'
12
- require 'MrMurano/Solution-File'
13
26
  require 'MrMurano/Solution-Services'
14
- require 'MrMurano/Solution-Users'
15
27
  require 'MrMurano/Solution-ServiceConfig'
16
- require 'MrMurano/Product'
17
- require 'MrMurano/Product-1P-Device'
18
- require 'MrMurano/Product-Resources'
28
+ require 'MrMurano/Solution-Users'
29
+ require 'MrMurano/Webservice-Cors'
30
+ require 'MrMurano/Webservice-Endpoint'
31
+ require 'MrMurano/Webservice-File'
19
32
 
20
- #require 'MrMurano/ReCommander'
33
+ require 'MrMurano/ReCommander'
21
34
  require 'MrMurano/commands'
35
+
@@ -1,221 +1,206 @@
1
- require 'uri'
2
- require 'net/http'
3
- require 'json'
1
+ # Last Modified: 2017.08.17 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
7
+
4
8
  require 'date'
9
+ require 'json'
10
+ require 'net/http'
5
11
  require 'pathname'
12
+ require 'uri'
6
13
  require 'yaml'
7
- require 'MrMurano/Config'
14
+ require 'MrMurano/hash'
8
15
  require 'MrMurano/http'
9
16
  require 'MrMurano/verbosing'
17
+ require 'MrMurano/Business'
18
+ require 'MrMurano/Config'
19
+ require 'MrMurano/Passwords'
20
+ require 'MrMurano/Solution'
10
21
 
11
22
  module MrMurano
12
- class Passwords
23
+ class Account
24
+ # The tool only works for a single user. To avoid fetching the
25
+ # token multiple times (and to avoid having to pass an Account
26
+ # object around), we make the class a singleton.
27
+ include Singleton
28
+
29
+ include Http
13
30
  include Verbose
14
- def initialize(path=nil)
15
- path = $cfg.file_at('passwords', :user) if path.nil?
16
- path = Pathname.new(path) unless path.kind_of? Pathname
17
- @path = path
18
- @data = nil
19
- end
20
- def load()
21
- if @path.exist? then
22
- @path.chmod(0600)
23
- @path.open('rb') do |io|
24
- @data = YAML.load(io)
25
- end
26
- end
27
- end
28
- def save()
29
- @path.dirname.mkpath unless @path.dirname.exist?
30
- @path.open('wb') do |io|
31
- io << @data.to_yaml
32
- end
33
- @path.chmod(0600)
34
- end
35
- def set(host, user, pass)
36
- unless @data.kind_of? Hash then
37
- @data = {host=>{user=>pass}}
38
- return
39
- end
40
- hd = @data[host]
41
- if hd.nil? or not hd.kind_of?(Hash) then
42
- @data[host] = {user=>pass}
43
- return
44
- end
45
- @data[host][user] = pass
46
- return
31
+
32
+ def initialize
33
+ @token = nil
47
34
  end
48
- def get(host, user)
49
- return ENV['MURANO_PASSWORD'] unless ENV['MURANO_PASSWORD'].nil?
50
- unless ENV['MR_PASSWORD'].nil? then
51
- warning %{Using depercated ENV "MR_PASSWORD", please rename to "MURANO_PASSWORD"}
52
- return ENV['MR_PASSWORD']
53
- end
54
- return nil unless @data.kind_of? Hash
55
- return nil unless @data.has_key? host
56
- return nil unless @data[host].kind_of? Hash
57
- return nil unless @data[host].has_key? user
58
- return @data[host][user]
35
+
36
+ def host
37
+ $cfg['net.host'].to_s
59
38
  end
60
39
 
61
- ## Remove the password for a user
62
- def remove(host, user)
63
- if @data.kind_of? Hash then
64
- hd = @data[host]
65
- if not hd.nil? and hd.kind_of?(Hash) then
66
- if hd.has_key? user then
67
- @data[host].delete user
68
- end
69
- end
70
- end
40
+ def user
41
+ $cfg['user.name'].to_s
71
42
  end
72
43
 
73
- ## Get all hosts and usernames. (does not return the passwords)
74
- def list
75
- ret = {}
76
- @data.each_pair{|key,value| ret[key] = value.keys} unless @data.nil?
77
- ret
44
+ def endpoint(path)
45
+ URI('https://' + host + '/api:1/' + path.to_s)
78
46
  end
79
- end
80
47
 
81
- class Account
82
- include Http
83
- include Verbose
48
+ # ---------------------------------------------------------------------
84
49
 
85
- def endPoint(path)
86
- URI('https://' + $cfg['net.host'] + '/api:1/' + path.to_s)
87
- end
50
+ LOGIN_ADVICE = %(
51
+ Please login using `murano login` or `murano init`.
52
+ Or set your password with `murano password set <username>`.
53
+ ).strip
54
+ LOGIN_NOTICE = 'Please login.'
88
55
 
89
- def _loginInfo
90
- host = $cfg['net.host']
91
- user = $cfg['user.name']
92
- if user.nil? or user.empty? then
93
- error("No Murano user account found; please login")
94
- user = ask("User name: ")
95
- $cfg.set('user.name', user, :user)
56
+ def login_info
57
+ warned_once = false
58
+ if user.empty?
59
+ prologue = 'No Murano user account found.'
60
+ unless $cfg.prompt_if_logged_off
61
+ MrMurano::Verbose.whirly_stop
62
+ error("#{prologue}\n#{LOGIN_ADVICE}")
63
+ exit 2
64
+ end
65
+ MrMurano::Verbose.whirly_pause
66
+ error("#{prologue} #{LOGIN_NOTICE}")
67
+ warned_once = true
68
+ username = ask('User name: ')
69
+ $cfg.set('user.name', username, :user)
70
+ $project.refresh_user_name
71
+ MrMurano::Verbose.whirly_unpause
96
72
  end
97
- pff = $cfg.file_at('passwords', :user)
98
- pf = Passwords.new(pff)
99
- pf.load
100
- pws = pf.get(host, user)
101
- if pws.nil? then
102
- error("Couldn't find password for #{user}")
103
- pws = ask("Password: ") { |q| q.echo = "*" }
104
- pf.set(host, user, pws)
105
- pf.save
73
+ pwd_path = $cfg.file_at('passwords', :user)
74
+ pwd_file = MrMurano::Passwords.new(pwd_path)
75
+ pwd_file.load
76
+ user_pass = pwd_file.get(host, user)
77
+ if user_pass.nil?
78
+ prologue = "No Murano password found for #{user}."
79
+ unless $cfg.prompt_if_logged_off
80
+ MrMurano::Verbose.whirly_stop
81
+ error("#{prologue}\n#{LOGIN_ADVICE}")
82
+ exit 2
83
+ end
84
+ MrMurano::Verbose.whirly_pause
85
+ error(%(#{prologue} #{LOGIN_NOTICE}).strip) unless warned_once
86
+ user_pass = ask('Password: ') { |q| q.echo = '*' }
87
+ pwd_file.set(host, user, user_pass)
88
+ pwd_file.save
89
+ MrMurano::Verbose.whirly_unpause
106
90
  end
107
- {
108
- :email => $cfg['user.name'],
109
- :password => pws
91
+ creds = {
92
+ email: user,
93
+ password: user_pass,
110
94
  }
95
+ creds
111
96
  end
112
97
 
113
- # Store the token in a class variable so that we only fetch it once per run
114
- # session of this tool
115
- @@token = nil
98
+ # ---------------------------------------------------------------------
99
+
116
100
  def token
117
- if @@token.nil? then
118
- # Cannot have token call token, so cannot use workit.
119
- uri = endPoint('token/')
120
- request = Net::HTTP::Post.new(uri)
121
- request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
122
- request.content_type = 'application/json'
123
- curldebug(request)
124
- #request.basic_auth(username(), password())
125
- request.body = JSON.generate(_loginInfo)
126
-
127
- response = http.request(request)
128
- case response
129
- when Net::HTTPSuccess
130
- token = JSON.parse(response.body, json_opts)
131
- @@token = token[:token]
132
- else
133
- showHttpError(request, response)
134
- error "Check to see if username and password are correct."
135
- @@token = nil
136
- end
137
- end
138
- @@token
101
+ token_fetch if @token.to_s.empty?
102
+ @token
139
103
  end
140
104
 
141
105
  def token_reset(value=nil)
142
- @@token = value
143
- end
144
-
145
- def new_account(email, name, company="")
146
- post('key/', {
147
- :email=>email,
148
- :name=>name,
149
- :company=>company,
150
- :source=>'signup',
151
- })
152
- end
153
-
154
- def reset_account(email)
155
- post('key/', { :email=>email, :source=>'reset' })
106
+ @token = value
107
+ end
108
+
109
+ def token_fetch
110
+ # Cannot have token call token, so cannot use Http::workit.
111
+ uri = endpoint('token/')
112
+ request = Net::HTTP::Post.new(uri)
113
+ request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
114
+ request.content_type = 'application/json'
115
+ curldebug(request)
116
+ #request.basic_auth(username(), password())
117
+ request.body = JSON.generate(login_info)
118
+
119
+ MrMurano::Verbose.whirly_start('Logging in...')
120
+ response = http.request(request)
121
+ MrMurano::Verbose.whirly_stop
122
+
123
+ case response
124
+ when Net::HTTPSuccess
125
+ token = JSON.parse(response.body, json_opts)
126
+ @token = token[:token]
127
+ else
128
+ showHttpError(request, response)
129
+ error 'Check to see if username and password are correct.'
130
+ unless ENV['MURANO_PASSWORD'].to_s.empty?
131
+ pwd_path = $cfg.file_at('passwords', :user)
132
+ warning "BEWARE: The password used was from MURANO_PASSWORD, not from #{pwd_path}"
133
+ end
134
+ @token = nil
135
+ end
156
136
  end
157
137
 
158
- def accept_account(token, password)
159
- post("key/#{token}", {:password=>password})
160
- end
138
+ # ---------------------------------------------------------------------
139
+
140
+ def businesses(bid: nil, name: nil, fuzzy: nil)
141
+ # Ask user for name and password, if not saved to config and password files.
142
+ login_info if user.empty?
143
+ raise 'Missing user?!' if user.empty?
144
+
145
+ MrMurano::Verbose.whirly_start 'Fetching Businesses...'
146
+ bizes = get('user/' + user + '/membership/')
147
+ MrMurano::Verbose.whirly_stop
148
+ return [] unless bizes.is_a?(Array) && bizes.any?
149
+
150
+ # 2017-06-30: The data for each message contains a :bizid, :role, and :name.
151
+ # :role is probably generally "owner".
152
+
153
+ match_bid = ensure_array(bid)
154
+ match_name = ensure_array(name)
155
+ match_fuzzy = ensure_array(fuzzy)
156
+ if match_bid.any? || match_name.any? || match_fuzzy.any?
157
+ bizes.select! do |biz|
158
+ (
159
+ match_bid.include?(biz[:bizid]) ||
160
+ match_name.include?(biz[:name]) ||
161
+ match_termy.any? do |term|
162
+ biz[:name] =~ /#{Regexp.escape(term)}/i || biz[:bizid] =~ /#{Regexp.escape(term)}/i
163
+ end
164
+ )
165
+ end
166
+ end
161
167
 
162
- def businesses
163
- _loginInfo if $cfg['user.name'].nil?
164
- get('user/' + $cfg['user.name'] + '/membership/')
165
- end
168
+ bizes.map! { |meta| MrMurano::Business.new(meta) }
166
169
 
167
- def new_business(name)
168
- post('business/', {:name=>name})
170
+ # Sort results.
171
+ bizes.sort_by!(&:name)
169
172
  end
170
173
 
171
- def delete_business(id)
172
- delete("business/#{id}")
173
- end
174
+ # ---------------------------------------------------------------------
174
175
 
175
- def has_projects?(id)
176
- ret = get("business/#{id}/overview")
177
- return false unless ret.kind_of? Hash
178
- return false unless ret.has_key? :tier
179
- tier = ret[:tier]
180
- return false unless tier.kind_of? Hash
181
- return false unless tier.has_key? :enableProjects
182
- return tier[:enableProjects]
183
- end
176
+ # 2017-07-05: [lb] notes that the remaining methods are not called.
177
+ # (Tilstra might be calling these via the _qb plugin.)
184
178
 
185
- def products
186
- raise "Missing Business ID" if $cfg['business.id'].nil?
187
- get('business/' + $cfg['business.id'] + '/product/')
179
+ def new_account(email, name, company='')
180
+ # this is a kludge. If we're gonna support this, do it better.
181
+ @token = ''
182
+ post('key/', email: email, name: name, company: company, source: 'signup')
188
183
  end
189
184
 
190
- ## Create a new product in the current business
191
- def new_product(name, type='onepModel')
192
- raise "Missing Business ID" if $cfg['business.id'].nil?
193
- post('business/' + $cfg['business.id'] + '/product/', {:label=>name, :type=>type})
185
+ def reset_account(email)
186
+ post('key/', email: email, source: 'reset')
194
187
  end
195
188
 
196
- def delete_product(modelId)
197
- raise "Missing Business ID" if $cfg['business.id'].nil?
198
- delete('business/' + $cfg['business.id'] + '/product/' + modelId)
189
+ def accept_account(token, password)
190
+ # this is a kludge. If we're gonna support this, do it better.
191
+ @token = ''
192
+ post("key/#{token}", password: password)
199
193
  end
200
194
 
201
- def solutions
202
- raise "Missing Business ID" if $cfg['business.id'].nil?
203
- get('business/' + $cfg['business.id'] + '/solution/')
204
- end
195
+ # ---------------------------------------------------------------------
205
196
 
206
- ## Create a new solution
207
- def new_solution(name, type='dataApi')
208
- raise "Missing Business ID" if $cfg['business.id'].nil?
209
- raise "Solution name must be a valid domain name component" unless name.match(/^[a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9]{0,1}|[a-zA-Z0-9]{0,62})$/)
210
- post('business/' + $cfg['business.id'] + '/solution/', {:label=>name, :type=>type})
197
+ def new_business(name)
198
+ post('business/', name: name)
211
199
  end
212
200
 
213
- def delete_solution(apiId)
214
- raise "Missing Business ID" if $cfg['business.id'].nil?
215
- delete('business/' + $cfg['business.id'] + '/solution/' + apiId)
201
+ def delete_business(id)
202
+ delete("business/#{id}")
216
203
  end
217
-
218
204
  end
219
205
  end
220
206
 
221
- # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,381 @@
1
+ # Last Modified: 2017.08.16 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
7
+
8
+ require 'date'
9
+ require 'inflecto'
10
+ require 'json'
11
+ require 'net/http'
12
+ require 'pathname'
13
+ require 'rainbow'
14
+ require 'uri'
15
+ require 'yaml'
16
+ require 'MrMurano/hash'
17
+ require 'MrMurano/http'
18
+ require 'MrMurano/verbosing'
19
+ require 'MrMurano/Config'
20
+ require 'MrMurano/Solution'
21
+ require 'MrMurano/Passwords'
22
+
23
+ module MrMurano
24
+ # The Business class represents an end user's solutions.
25
+ class Business
26
+ include Http
27
+ include Verbose
28
+
29
+ #attr_accessor :bid
30
+ #attr_accessor :name
31
+ attr_accessor :role
32
+ attr_reader :meta
33
+ attr_writer :bid
34
+ attr_writer :name
35
+
36
+ def initialize(data=nil)
37
+ @bid = nil
38
+ @name = nil
39
+ @valid = false
40
+ @user_bizes = {}
41
+ self.meta = data unless data.nil?
42
+ end
43
+
44
+ def valid?
45
+ @valid
46
+ end
47
+
48
+ def bid
49
+ return @bid unless @bid.to_s.empty?
50
+ $cfg['business.id'].to_s
51
+ end
52
+
53
+ def name
54
+ return @name unless @name.to_s.empty?
55
+ $cfg['business.name'].to_s
56
+ end
57
+
58
+ def bizid
59
+ bid
60
+ end
61
+
62
+ def ==(other)
63
+ other.class == self.class && other.state == state
64
+ end
65
+
66
+ protected
67
+
68
+ def state
69
+ [bid, name, @valid, @user_bizes]
70
+ end
71
+
72
+ public
73
+
74
+ # Consume data returned from Account::businesses.
75
+ def meta=(data)
76
+ @valid = !data.nil?
77
+ return unless @valid
78
+ @bid = data[:bizid]
79
+ @name = data[:name]
80
+ @role = data[:role]
81
+ @meta = data
82
+ end
83
+
84
+ def write(scope=:project)
85
+ $cfg.set('business.id', bid, scope)
86
+ $cfg.set('business.name', name, scope)
87
+ self
88
+ end
89
+
90
+ # MAYBE: Check that ADC is enabled on the business. If not, tell
91
+ # user to run Murano 2.x. [lb] is not sure which value from
92
+ # Murano to check. Is it :enableMurano or :enableProjects?
93
+ # See the overview method.
94
+ #def adc_compat_check
95
+ # unless $cfg['business.id'].nil?
96
+ # unless projects?($cfg['business.id'])
97
+ # # This is 3.x which does not support projects!
98
+ # warning('!'*80)
99
+ # warning "Your business requires Murano CLI 2.x"
100
+ # warning "Some features may not work correctly."
101
+ # warning('!'*80)
102
+ # end
103
+ # end
104
+ #end
105
+
106
+ def must_business_id!
107
+ raise MrMurano::ConfigError.new(Business.missing_business_id_msg) if bid.to_s.empty?
108
+ end
109
+
110
+ def self.missing_business_id_msg
111
+ %(
112
+ Missing Business ID.
113
+ Call `#{MrMurano::EXE_NAME} business list` to get a list of business IDs.
114
+ Set the ID temporarily using --config business.id=<ID>
115
+ or add to the project config using \`#{MrMurano::EXE_NAME} config business.id <ID>\`
116
+ or add to the user config using \`#{MrMurano::EXE_NAME} config business.id <ID> --user\`
117
+ or set it interactively using \`#{MrMurano::EXE_NAME} init\`
118
+ ).strip
119
+ end
120
+
121
+ def pretty_name_and_id
122
+ "‘#{Rainbow(name).underline}’ <#{bid}>"
123
+ end
124
+
125
+ # ---------------------------------------------------------------------
126
+
127
+ #def projects?(id)
128
+ # ret = get("business/#{id}/overview")
129
+ # return false unless ret.is_a?(Hash)
130
+ # return false unless ret.key?(:tier)
131
+ # tier = ret[:tier]
132
+ # return false unless tier.is_a?(Hash)
133
+ # return false unless tier.key?(:enableProjects)
134
+ # tier[:enableProjects]
135
+ #end
136
+
137
+ def overview(&block)
138
+ # Here are all the goodies that the overview endpoint returns:
139
+ # {:name=>"XXX", :email=>"XXX", :contact=>"XXX",
140
+ # :billing=>{
141
+ # :terms=>0, :balance=>0, :overdue=>0},
142
+ # :tier=>{
143
+ # :name=>"Community", :id=>"free", :price=>0, :users=>nil, :domains=>0,
144
+ # :ssl=>false, :rebrand=>false, :billing=>false, :multisolution=>true,
145
+ # :phonesupport=>false, :supportLevel=>"Community", :servicesTier=>"Core",
146
+ # :onboarding=>{}, :enableMurano=>true, :enableProjects=>true},
147
+ # :accountManager=>{
148
+ # :name=>"", :email=>"", :phone=>""},
149
+ # :accountLimits=>{
150
+ # :teamMembers=>1, :solutionApis=>1, :productModels=>1,
151
+ # :perModelDevices=>10, :perApiUsers=>10}, :lineitems=>[]}
152
+ # EXPLAIN/2017-06-30: Which value(s) tell us if ADC is enabled?
153
+ whirly_start('Fetching Business...')
154
+ data = get("business/#{bid}/overview", &block)
155
+ whirly_stop
156
+ @valid = !data.nil?
157
+ @name = data[:name] if @valid
158
+ @valid
159
+ end
160
+
161
+ # ---------------------------------------------------------------------
162
+
163
+ # 2017-06-30: In ADC-enabled Murano, there are now just 2 solution types.
164
+ # LATER: This'll change in the future; Murano will support arbitrary
165
+ # solution types.
166
+ #ALLOWED_TYPES = [:domain, :onepApi, :dataApi, :application, :product,].freeze
167
+ ALLOWED_TYPES = %i[application product].freeze
168
+
169
+ def solutions(type: :all, sid: nil, name: nil, fuzzy: nil, invalidate: false)
170
+ debug "Getting all solutions of type #{type}"
171
+ must_business_id!
172
+
173
+ type = type.to_sym
174
+ raise "Unknown type(#{type})" unless type == :all || ALLOWED_TYPES.include?(type)
175
+
176
+ # Cache the result since sometimes both products() and applications() are called.
177
+ if invalidate || @user_bizes[type].nil?
178
+ if invalidate || @user_bizes[:all].nil?
179
+ got = get('business/' + bid + '/solution/') do |request, http|
180
+ response = http.request(request)
181
+ case response
182
+ when Net::HTTPSuccess
183
+ workit_response(response)
184
+ when Net::HTTPForbidden # 403
185
+ # FIXME/CONFIRM/2017-07-13: Is this really what platform
186
+ # says when business has no solutions? I do not remember
187
+ # seeing this before... [lb]
188
+ nil
189
+ else
190
+ showHttpError(request, response)
191
+ end
192
+ end
193
+
194
+ @user_bizes[:all] = got || []
195
+ end
196
+
197
+ if invalidate || @user_bizes[type].nil?
198
+ @user_bizes[type] = @user_bizes[:all].select { |i| i[:type] == type.to_s }
199
+ end
200
+ end
201
+
202
+ solz = @user_bizes[type].dup
203
+
204
+ match_sid = ensure_array(sid)
205
+ match_name = ensure_array(name)
206
+ match_fuzzy = ensure_array(fuzzy)
207
+ if match_sid.any? || match_name.any? || match_fuzzy.any?
208
+ solz.select! do |sol|
209
+ (
210
+ match_sid.include?(sol[:apiId]) ||
211
+ match_name.include?(sol[:name]) ||
212
+ match_fuzzy.any? do |term|
213
+ sol[:name] =~ /#{Regexp.escape(term)}/i || sol[:apiId] =~ /#{Regexp.escape(term)}/i
214
+ end
215
+ )
216
+ end
217
+ end
218
+
219
+ solz.map! do |meta|
220
+ case meta[:type].to_sym
221
+ when :application
222
+ MrMurano::Application.new(meta)
223
+ when :product
224
+ MrMurano::Product.new(meta)
225
+ else
226
+ warning("Unexpected solution type: #{meta[:type]}")
227
+ warning('* Please enable Murano for this business') if meta[:type].to_sym == :dataApi
228
+ MrMurano::Solution.new(meta)
229
+ end
230
+ end
231
+
232
+ sort_solutions!(solz)
233
+ end
234
+
235
+ def sort_solutions!(solz)
236
+ solz.sort_by!(&:name)
237
+ solz.sort_by! { |sol| ALLOWED_TYPES.index(sol.type) }
238
+ end
239
+
240
+ ## Given a type (:application or :product), return a Solution instance.
241
+ def solution_from_type!(type)
242
+ type = type.to_s.to_sym
243
+ raise "Unknown type(#{type})" unless type.to_s.empty? || ALLOWED_TYPES.include?(type)
244
+ sid = MrMurano::Solution::INVALID_SID
245
+ if type == :application
246
+ sol = MrMurano::Application.new(sid)
247
+ elsif type == :product
248
+ sol = MrMurano::Product.new(sid)
249
+ else
250
+ #raise "Unexpected path: Unrecognized type ‘#{type}’"
251
+ sol = MrMurano::Solution.new(sid)
252
+ end
253
+ sol.biz = self
254
+ sol
255
+ end
256
+
257
+ ## Create a new solution in the current business
258
+ def new_solution!(name, type)
259
+ must_business_id!
260
+ sol = solution_from_type!(type)
261
+ sol.set_name!(name)
262
+ if $cfg['tool.dry']
263
+ say "--dry: Not creating solution #{name}"
264
+ return nil
265
+ end
266
+ whirly_start 'Creating solution...'
267
+ resp = post(
268
+ 'business/' + bid + '/solution/',
269
+ label: sol.name,
270
+ type: sol.type,
271
+ ) do |request, http|
272
+ response = http.request(request)
273
+ MrMurano::Verbose.whirly_stop
274
+ if response.is_a?(Net::HTTPSuccess)
275
+ workit_response(response)
276
+ else
277
+ MrMurano::Verbose.error(
278
+ "Unable to create #{sol.type_name}: ‘#{sol.name}’"
279
+ )
280
+ ok = false
281
+ if response.is_a?(Net::HTTPConflict)
282
+ _isj, jsn = isJSON(response.body)
283
+ if jsn[:message] == 'limit'
284
+ ok = true
285
+ MrMurano::Verbose.error(
286
+ "You've reached your limit of #{Inflecto.pluralize(sol.type.to_s)}."
287
+ )
288
+ else
289
+ ok = false
290
+ end
291
+ end
292
+ showHttpError(request, response) unless ok
293
+ # Hard stop.
294
+ exit 1
295
+ nil
296
+ end
297
+ end
298
+ whirly_stop
299
+ new_solution_prepare!(sol, resp)
300
+ end
301
+
302
+ def new_solution_prepare!(sol, resp)
303
+ if resp.nil?
304
+ error("Create #{sol.type_name} failed: Request failed")
305
+ exit 1
306
+ end
307
+ unless resp.is_a?(Hash)
308
+ error("Create #{sol.type_name} failed: Unexpected response: #{resp}")
309
+ exit 1
310
+ end
311
+ if resp[:id].to_s.empty?
312
+ error("Unexpected: Solution ID not returned: #{resp}")
313
+ exit 1
314
+ end
315
+ sol.sid = resp[:id]
316
+ sol.affirm_valid
317
+ # 2017-06-29: The code used to hunt for the solution ID, because
318
+ # POST business/<bizid>/solution/ used to not return anything,
319
+ # but now it returns the solution ID.
320
+ # FIXME: Delete this eventually, once you verify the new behavior.
321
+ #if false
322
+ # # Create doesn't return anything, so go looking for it.
323
+ # MrMurano::Verbose.whirly_start('Verifying solution...')
324
+ # invalidate_cache = true
325
+ # ret = solutions(sol.type, invalidate_cache).select do |meta|
326
+ # meta[:name] == sol.name || meta[:domain] =~ /#{sol.name}\./i
327
+ # end
328
+ # MrMurano::Verbose.whirly_stop
329
+ # if ret.count > 1
330
+ # warning("Found more than 1 matching solution: #{ret}")
331
+ # elsif ret.count.zero?
332
+ # error("Unable to verify solution created for ‘#{sol.name}’: #{ret}")
333
+ # exit 3
334
+ # end
335
+ # sol.meta = ret.first
336
+ # if sol.sid.to_s.empty? then
337
+ # error("New solution created for ‘#{sol.name}’ missing ID?: #{ret}")
338
+ # exit 3
339
+ # end
340
+ # sol.sid = sid
341
+ #end
342
+ sol
343
+ end
344
+
345
+ def delete_solution(sid)
346
+ must_business_id!
347
+ delete('business/' + bid + '/solution/' + sid)
348
+ end
349
+
350
+ # ---------------------------------------------------------------------
351
+
352
+ def products(**options)
353
+ solutions(type: :product, **options)
354
+ end
355
+
356
+ ## Create a new product in the current business
357
+ def new_product(name, type=:product)
358
+ new_solution!(name, type)
359
+ end
360
+
361
+ def delete_product(sid)
362
+ delete_solution(sid)
363
+ end
364
+
365
+ # ---------------------------------------------------------------------
366
+
367
+ def applications(**options)
368
+ solutions(type: :application, **options)
369
+ end
370
+
371
+ ## Create a new application in the current business
372
+ def new_application(name, type=:application)
373
+ new_solution!(name, type)
374
+ end
375
+
376
+ def delete_application(sid)
377
+ delete_solution(sid)
378
+ end
379
+ end
380
+ end
381
+