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
@@ -0,0 +1,191 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'net/http'
4
+ require 'mime/types'
5
+ require 'digest'
6
+ require 'http/form_data'
7
+ require 'MrMurano/Config'
8
+ require 'MrMurano/http'
9
+ require 'MrMurano/verbosing'
10
+ require 'MrMurano/SolutionId'
11
+ require 'MrMurano/SyncUpDown'
12
+
13
+ module MrMurano
14
+ ## The details of talking to the Content service.
15
+ module Content
16
+ class Base
17
+ include Http
18
+ include Verbose
19
+ include SolutionId
20
+
21
+ def initialize
22
+ @solntype = 'product.id'
23
+ @uriparts_sidex = 1
24
+ init_sid!
25
+ @uriparts = [:service, @sid, :content, :item]
26
+ @itemkey = :id
27
+ #@locationbase = $cfg['location.base']
28
+ @location = nil
29
+ end
30
+
31
+ ## Generate an endpoint in Murano
32
+ # Uses the uriparts and path
33
+ # @param path String: any additional parts for the URI
34
+ # @return URI: The full URI for this enpoint.
35
+ def endpoint(path='')
36
+ super
37
+ parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
38
+ s = parts.map{|v| v.to_s}.join('/')
39
+ URI(s + path.to_s)
40
+ end
41
+
42
+ # List of what is in the content area?
43
+ def list
44
+ get('?full=true')
45
+ # MAYBE/2017-08-17:
46
+ # ret = get('?full=true')
47
+ # return [] unless ret.is_a?(Array)
48
+ # sort_by_name(ret)
49
+ end
50
+
51
+ # Delete Everything in you content area
52
+ def clear_all
53
+ delete('')
54
+ end
55
+
56
+ # Get details of a single item in content area
57
+ # @param name [String] Name of content
58
+ def fetch(name)
59
+ get("/#{CGI.escape(name)}")
60
+ end
61
+ alias info fetch
62
+
63
+ # Upload content to area.
64
+ # @param name [String] Name of content to be uploaded.
65
+ # @param local_path [String, Pathname] The file to upload
66
+ # @param tags [Hash] Extra meta to attach to this file.
67
+ def upload(name, local_path, tags=nil)
68
+ # This is a two step process.
69
+ # 1: Get the post instructions for S3.
70
+ # 2: Upload to S3.
71
+
72
+ # ?tags=CGI.escape(meta.to_json)
73
+ # ?type=
74
+ sha256 = Digest::SHA256.new
75
+ sha256.file(local_path.to_s)
76
+ mime = MIME::Types.type_for(local_path.to_s)[0] || MIME::Types["application/octet-stream"][0]
77
+
78
+ params = {
79
+ :sha256 => sha256.hexdigest,
80
+ :expires_in => 30,
81
+ :type => mime,
82
+ }
83
+ if not tags.nil? and tags.kind_of? Hash then
84
+ params[:tags] = tags.to_json
85
+ end
86
+
87
+ ret = get("/#{CGI.escape(name)}/upload?#{URI.encode_www_form(params)}")
88
+ debug "POST instructions: #{ret}"
89
+ raise "Method isn't POST!!!" unless ret.is_a?(Hash) && ret[:method] == 'POST'
90
+ raise "EncType isn't multipart/form-data" unless ret[:enctype] == 'multipart/form-data'
91
+
92
+ uri = URI(ret[:url])
93
+ request = Net::HTTP::Post.new(uri)
94
+ file = HTTP::FormData::File.new(local_path.to_s, {:content_type=>mime})
95
+ form = HTTP::FormData.create(ret[:inputs].merge({ret[:field]=>file}))
96
+
97
+ request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
98
+ request.content_type = form.content_type
99
+ request.content_length = form.content_length
100
+ request.body = form.to_s
101
+
102
+ if $cfg['tool.curldebug'] then
103
+ a = []
104
+ a << %{curl -s}
105
+ a << %{-H 'User-Agent: #{request['User-Agent']}'}
106
+ a << %{-X #{request.method}}
107
+ a << %{'#{request.uri.to_s}'}
108
+ ret[:inputs].each_pair do |key, value|
109
+ a << %{-F '#{key}=#{value}'}
110
+ end
111
+ a << %{-F #{ret[:field]}=@#{local_path.to_s}}
112
+ if $cfg.curlfile_f.nil?
113
+ puts a.join(' ')
114
+ else
115
+ $cfg.curlfile_f << a.join(' ') + "\n\n"
116
+ $cfg.curlfile_f.flush
117
+ end
118
+ end
119
+
120
+ unless $cfg['tool.dry'] then
121
+ Net::HTTP.start(uri.host, uri.port, {:use_ssl=>true}) do |ihttp|
122
+ response = ihttp.request(request)
123
+ case response
124
+ when Net::HTTPSuccess
125
+ else
126
+ showHttpError(request, response)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Remove content by name
133
+ # @param name [String] Name of content to be deleted
134
+ def remove(name)
135
+ delete("/#{CGI.escape(name)}")
136
+ end
137
+
138
+ # Download content
139
+ # @param name [String] Name of content to be downloaded
140
+ # @param block [Block] Block to process data as it is downloaded
141
+ def download(name, &block)
142
+ # This is a two step process.
143
+ # 1: Get the get instructions for S3.
144
+ # 2: fetch from S3.
145
+ ret = get("/#{CGI.escape(name)}/download")
146
+ debug "GET instructions: #{ret}"
147
+ raise "Method isn't GET!!!" unless ret.is_a?(Hash) && ret[:method] == 'GET'
148
+
149
+ uri = URI(ret[:url])
150
+ request = Net::HTTP::Get.new(uri)
151
+ request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
152
+
153
+ if $cfg['tool.curldebug'] then
154
+ a = []
155
+ a << %{curl -s}
156
+ a << %{-H 'User-Agent: #{request['User-Agent']}'}
157
+ a << %{-X #{request.method}}
158
+ a << %{'#{request.uri.to_s}'}
159
+ if $cfg.curlfile_f.nil?
160
+ puts a.join(' ')
161
+ else
162
+ $cfg.curlfile_f << a.join(' ') + "\n\n"
163
+ $cfg.curlfile_f.flush
164
+ end
165
+ end
166
+
167
+ unless $cfg['tool.dry'] then
168
+ Net::HTTP.start(uri.host, uri.port, {:use_ssl=>true}) do |ihttp|
169
+ ihttp.request(request) do |response|
170
+ case response
171
+ when Net::HTTPSuccess
172
+ if block_given? then
173
+ response.read_body(&block)
174
+ else
175
+ response.read_body do |chunk|
176
+ $stdout.write chunk
177
+ end
178
+ end
179
+ else
180
+ showHttpError(request, response)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+
191
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,489 @@
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
+
8
+ require 'http/form_data'
9
+ require 'json-schema'
10
+ require 'net/http'
11
+ require 'uri'
12
+ require 'MrMurano/hash'
13
+ require 'MrMurano/http'
14
+ require 'MrMurano/verbosing'
15
+ require 'MrMurano/Config'
16
+ require 'MrMurano/SolutionId'
17
+ require 'MrMurano/SyncRoot'
18
+ require 'MrMurano/SyncUpDown'
19
+
20
+ module MrMurano
21
+ ## The details of talking to the Gateway [Device2] service.
22
+ # This is where interfacing to real hardware happens.
23
+ module Gateway
24
+ class GweBase
25
+ include Http
26
+ include Verbose
27
+ include SolutionId
28
+
29
+ def initialize
30
+ @solntype = 'product.id'
31
+ @uriparts_sidex = 1
32
+ init_sid!
33
+ @uriparts = [:service, @sid, :device2]
34
+ @uriparts_sidex = 1
35
+ @itemkey = :id
36
+ end
37
+
38
+ ## Generate an endpoint in Murano
39
+ # Uses the uriparts and path
40
+ # @param path String: any additional parts for the URI
41
+ # @return URI: The full URI for this enpoint.
42
+ def endpoint(path='')
43
+ super
44
+ parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
45
+ s = parts.map(&:to_s).join('/')
46
+ URI(s + path.to_s)
47
+ end
48
+ # …
49
+ #include SyncUpDown
50
+
51
+ # Get info for this gateway interface.
52
+ def info
53
+ get
54
+ end
55
+ end
56
+
57
+ class Settings < GweBase
58
+ # Get the protocol settings
59
+ def protocol
60
+ ret = get
61
+ return {} unless ret.is_a?(Hash)
62
+ return {} unless ret.key?(:protocol)
63
+ return {} unless ret[:protocol].is_a?(Hash)
64
+ ret[:protocol]
65
+ end
66
+
67
+ # Set the protocol settings
68
+ def protocol=(x)
69
+ raise 'Not Hash' unless x.is_a?(Hash)
70
+ x.delete_if { |k, _v| !%i[name devmode].include?(k) }
71
+ patch('', protocol: x)
72
+ end
73
+
74
+ def identity_format
75
+ ret = get
76
+ return {} unless ret.is_a?(Hash)
77
+ return {} unless ret.key?(:identity_format)
78
+ return {} unless ret[:identity_format].is_a?(Hash)
79
+ ret[:identity_format]
80
+ end
81
+
82
+ def identity_format=(x)
83
+ raise 'Not Hash' unless x.is_a?(Hash)
84
+ raise 'Not Hash' if x.key?(:options) && !x[:options].is_a?(Hash)
85
+ x.delete_if { |k, _v| !%i[type prefix options].include?(k) }
86
+ x[:options].delete_if { |k, _v| !%i[casing length].include?(k) }
87
+ patch('', identity_format: x)
88
+ end
89
+
90
+ def provisioning
91
+ ret = get
92
+ return {} unless ret.is_a?(Hash)
93
+ return {} unless ret.key?(:provisioning)
94
+ return {} unless ret[:provisioning].is_a?(Hash)
95
+ ret[:provisioning]
96
+ end
97
+
98
+ def provisioning=(x)
99
+ raise 'Not Hash' unless x.is_a?(Hash)
100
+ raise 'Not Hash' if x.key?(:ip_whitelisting) && !x[:ip_whitelisting].is_a?(Hash)
101
+ x.delete_if do |k, _v|
102
+ !%i[enabled auth_type generate_identity presenter_identity ip_whitelisting].include?(k)
103
+ end
104
+ x[:ip_whitelisting].delete_if { |k, _v| !%i[enabled allowed].include?(k) }
105
+ patch('', provisioning: x)
106
+ end
107
+ end
108
+
109
+ ##############################################################################
110
+ ## Working with the resources on a set of Devices. (Gateway)
111
+ class Resources < GweBase
112
+ include SyncUpDown
113
+
114
+ def initialize
115
+ super
116
+ @itemkey = :alias
117
+ @project_section = :resources
118
+ end
119
+
120
+ def self.description
121
+ %(Resource)
122
+ end
123
+
124
+ def list
125
+ ret = get
126
+ return [] unless ret.is_a?(Hash)
127
+ return [] unless ret.key?(:resources)
128
+
129
+ # convert hash to array.
130
+ res = []
131
+ ret[:resources].each_pair do |key, value|
132
+ res << value.merge(alias: key.to_s)
133
+ end
134
+ res
135
+ # MAYBE/2017-08-17:
136
+ # sort_by_name(res)
137
+ end
138
+
139
+ def upload_all(data)
140
+ # convert array to hash
141
+ res = {}
142
+ data.each do |value|
143
+ key = value[:alias]
144
+ res[key] = value.reject { |k, _v| k == :alias }
145
+ end
146
+
147
+ patch('', resources: res)
148
+ end
149
+
150
+ ###################################################
151
+
152
+ def syncup_before
153
+ super
154
+ @there = list
155
+ end
156
+
157
+ def remove(itemkey)
158
+ @there.delete_if { |item| item[@itemkey] == itemkey }
159
+ end
160
+
161
+ def upload(_local, remote, _modify)
162
+ @there.delete_if { |item| item[@itemkey] == remote[@itemkey] }
163
+ @there << remote.reject { |k, _v| %i[synckey synctype].include? k }
164
+ end
165
+
166
+ def syncup_after
167
+ super
168
+ if !@there.empty?
169
+ if !$cfg['tool.dry']
170
+ sync_update_progress('Updating product resources')
171
+ upload_all(@there)
172
+ else
173
+ MrMurano::Verbose.whirly_interject do
174
+ say('--dry: Not updating resources')
175
+ end
176
+ end
177
+ elsif $cfg['tool.verbose']
178
+ MrMurano::Verbose.whirly_interject do
179
+ say('No resources changed')
180
+ end
181
+ end
182
+ @there = nil
183
+ end
184
+
185
+ ###################################################
186
+
187
+ def syncdown_before
188
+ super
189
+ # TEST/2017-07-02: Could there be duplicate gateway items?
190
+ # [lb] just added code to SyncUpDown.locallist and is curious.
191
+ @here = locallist
192
+ end
193
+
194
+ def download(_local, item)
195
+ @here = locallist if @here.nil?
196
+ # needs to append/merge with file
197
+ @here.delete_if do |i|
198
+ i[@itemkey] == item[@itemkey]
199
+ end
200
+ @here << item.reject { |k, _v| %i[synckey synctype].include? k }
201
+ end
202
+
203
+ def diff_download(tmp_path, merged)
204
+ @there = list if @there.nil?
205
+ items = @there.select { |item| item[:alias] == merged[:alias] }
206
+ if items.length > 1
207
+ error(
208
+ "Unexpected: more than 1 resource with the same alias: #{merged[:alias]} / #{items}"
209
+ )
210
+ end
211
+ Pathname.new(tmp_path).open('wb') do |io|
212
+ if !items.length.zero?
213
+ diff_item_write(io, merged, nil, items.first)
214
+ else
215
+ io << "\n"
216
+ end
217
+ end
218
+ end
219
+
220
+ def removelocal(_local, item)
221
+ # needs to append/merge with file
222
+ key = @itemkey.to_sym
223
+ @here.delete_if do |it|
224
+ it[key] == item[key]
225
+ end
226
+ end
227
+
228
+ def syncdown_after(local)
229
+ super
230
+ resources_write(local)
231
+ @here = nil
232
+ end
233
+
234
+ def resources_write(file_path)
235
+ # User can blow away specs/ directory if they want; we'll just make
236
+ # a new one. [This code somewhat copy-paste from make_directory.]
237
+ basedir = file_path
238
+ basedir = basedir.dirname unless basedir.extname.empty?
239
+ raise 'Unexpected: bad basedir' if basedir.to_s.empty? || basedir == File::SEPARATOR
240
+
241
+ unless basedir.exist?
242
+ if $cfg['tool.dry']
243
+ MrMurano::Verbose.warning(
244
+ "--dry: Not creating default directory: #{basedir}"
245
+ )
246
+ else
247
+ FileUtils.mkdir_p(basedir, noop: $cfg['tool.dry'])
248
+ end
249
+ end
250
+
251
+ if $cfg['tool.dry']
252
+ MrMurano::Verbose.warning(
253
+ "--dry: Not writing resources file: #{file_path}"
254
+ )
255
+ return
256
+ end
257
+
258
+ file_path.open('wb') do |io|
259
+ # convert array to hash
260
+ res = {}
261
+ @here.each do |value|
262
+ key = value[:alias]
263
+ res[key] = Hash.transform_keys_to_strings(value.reject { |k, _v| k == :alias })
264
+ end
265
+ ohash = ordered_hash(res)
266
+ io.write ohash.to_yaml
267
+ end
268
+ end
269
+
270
+ def diff_item_write(io, _merged, local, remote)
271
+ raise 'Unexpected: please specify either local or remote, but not both' if local && remote
272
+ item = local || remote
273
+ raise "Unexpected: :local_path exists: #{item}" unless item[:local_path].to_s.empty?
274
+ res = {}
275
+ key = item[:alias]
276
+ item = item.reject { |k, _v| %i[alias synckey synctype].include? k }
277
+ res[key] = Hash.transform_keys_to_strings(item)
278
+ ohash = ordered_hash(res)
279
+ io << ohash.to_yaml
280
+ end
281
+
282
+ ###################################################
283
+
284
+ def tolocalpath(into, _item)
285
+ into
286
+ end
287
+
288
+ def localitems(from)
289
+ from = Pathname.new(from) unless from.is_a?(Pathname)
290
+ unless from.exist?
291
+ warning "Skipping missing #{from}"
292
+ return []
293
+ end
294
+ unless from.file?
295
+ warning "Cannot read from #{from}"
296
+ return []
297
+ end
298
+
299
+ here = {}
300
+ # rubocop:disable Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load.
301
+ # MAYBE/2017-07-02: Convert to safe_load.
302
+ from.open { |io| here = YAML.load(io) }
303
+ here = {} if here == false
304
+
305
+ # Validate file against schema.
306
+ schema_path = Pathname.new(File.dirname(__FILE__)) + 'schema/resource-v1.0.0.yaml'
307
+ # MAYBE/2017-07-03: Do we care if user duplicates keys in the yaml? See dup_count.
308
+ schema = YAML.load_file(schema_path.to_s)
309
+ begin
310
+ JSON::Validator.validate!(schema, here)
311
+ rescue JSON::Schema::ValidationError => err
312
+ error("There is an error in the config file, #{from}")
313
+ error(%("#{err.message}"))
314
+ exit 1
315
+ end
316
+
317
+ res = []
318
+ here.each_pair do |key, value|
319
+ res << Hash.transform_keys_to_symbols(value).merge(alias: key.to_s)
320
+ end
321
+
322
+ sort_by_name(res)
323
+ end
324
+
325
+ def docmp(item_a, item_b)
326
+ item_a != item_b
327
+ end
328
+ end
329
+ # 2017-07-18: Against OneP, fetching --resources is expensive, so this
330
+ # call was ignored bydefault (you'd have to add --resources to syncup,
331
+ # syncdown, diff, and status commands). Against Murano, this call seems
332
+ # normal speed, so including by default.
333
+ SyncRoot.instance.add('resources', Resources, 'R', true, %w[specs])
334
+
335
+ ##############################################################################
336
+ ##
337
+ # Talking to the devices on a Gateway
338
+ class Device < GweBase
339
+ def initialize
340
+ super
341
+ @uriparts << :identity
342
+ end
343
+
344
+ ## All devices (pagination?)
345
+ # @param limit [Number,String] How many devices to return
346
+ # @param before [Number,String] timestamp for something. TODO: want offset.
347
+ def list(limit=nil, before=nil)
348
+ pr = {}
349
+ pr[:limit] = limit unless limit.nil?
350
+ pr[:before] = before unless before.nil?
351
+ pr = nil if pr.empty?
352
+ get('/', pr)
353
+ # MAYBE/2017-08-17:
354
+ # ret = get('/', pr)
355
+ # return [] unless ret.is_a?(Array)
356
+ # sort_by_name(ret)
357
+ end
358
+
359
+ def query(args)
360
+ # TODO: actually just part of list.
361
+ # ?limit=#
362
+ # ?before=<time stamp in ms>
363
+ # ?status={whitelisted, provisioned, locked, devmode, reprovision}
364
+ # ?identity=<pattern>
365
+ # ?ipaddress=<pattern>
366
+ # ?version=<pattern>
367
+ end
368
+
369
+ ## Get one device
370
+ # @param id [String] The identity to fetch
371
+ def fetch(id)
372
+ get("/#{CGI.escape(id.to_s)}")
373
+ end
374
+
375
+ ## Create a device with given Identity
376
+ # @param id [String] The new identity
377
+ # @param opts [Hash] Options for the new device
378
+ # @option opts [String] :type One of: certificate, hash, password, signature, token
379
+ # @option opts [String,Pathname,IO] :publickey The certificate, or IO/Pathname to cert file
380
+ # @option opts [String] :privatekey Shared secret for hash, password, token types
381
+ # @option opts [String,Integer] :expire For Cert, when it must be reprovisioned, otherwise when the activation window closes.
382
+ def enable(id, opts=nil)
383
+ if !opts.nil?
384
+ #opts.reject! { |k, _v| !%i[type publickey privatekey expire].include?(k) }
385
+ opts.select! { |k, _v| %i[type publickey privatekey expire].include?(k) }
386
+ if opts.key?(:publickey) && !opts[:publickey].is_a?(String)
387
+ io = opts[:publickey]
388
+ opts[:publickey] = io.read
389
+ end
390
+ else
391
+ opts = {}
392
+ end
393
+ whirly_start('Enabling Device...')
394
+ putted = put("/#{CGI.escape(id.to_s)}", opts)
395
+ whirly_stop
396
+ putted
397
+ end
398
+ alias whitelist enable
399
+ alias create enable
400
+
401
+ ## Create a bunch of devices at once
402
+ # @param local [String, Pathname] CSV file of identifiers
403
+ # @param expire [Number] Expire time for all identities (ignored)
404
+ # @return [void]
405
+ def enable_batch(local, _expire=nil)
406
+ # Need to modify @uriparts for just this endpoint call.
407
+ uriparts = @uriparts
408
+ @uriparts[-1] = :identities
409
+ uri = endpoint
410
+ @uriparts = uriparts
411
+
412
+ file = HTTP::FormData::File.new(local.to_s, content_type: 'text/csv')
413
+ form = HTTP::FormData.create(identities: file)
414
+ req = Net::HTTP::Post.new(uri)
415
+ set_def_headers(req)
416
+ req.content_type = form.content_type
417
+ req.content_length = form.content_length
418
+ req.body = form.to_s
419
+ whirly_start('Enabling Devices...')
420
+ workit(req)
421
+ whirly_stop
422
+ nil
423
+ end
424
+
425
+ ## Delete a device
426
+ # @param identifier [String] Who to delete.
427
+ def remove(identifier)
428
+ delete("/#{CGI.escape(identifier.to_s)}")
429
+ end
430
+
431
+ # Call the device Activation URI.
432
+ #
433
+ # Only useful durring debugging of devices.
434
+ #
435
+ # @param identifier [String] Who to activate.
436
+ def activate(identifier)
437
+ info = GweBase.new.info
438
+ raise "Gateway info not found for #{identifier}" if info.nil?
439
+ fqdn = info[:fqdn]
440
+ debug "Found FQDN: #{fqdn}"
441
+ fqdn = "#{@sid}.m2.exosite.io" if fqdn.nil?
442
+
443
+ uri = URI("https://#{fqdn}/provision/activate")
444
+ http = Net::HTTP.new(uri.host, uri.port)
445
+ http.use_ssl = true
446
+ http.start
447
+ request = Net::HTTP::Post.new(uri)
448
+ request.form_data = {
449
+ vendor: @sid,
450
+ model: @sid,
451
+ sn: identifier,
452
+ }
453
+ request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
454
+ request['Authorization'] = nil
455
+ request.content_type = 'application/x-www-form-urlencoded; charset=utf-8'
456
+ curldebug(request)
457
+
458
+ whirly_start('Activating Device...')
459
+ response = http.request(request)
460
+ whirly_stop
461
+
462
+ case response
463
+ when Net::HTTPSuccess
464
+ return response.body
465
+ when Net::HTTPConflict
466
+ error('The specified device is already activated.')
467
+ exit 1
468
+ else
469
+ showHttpError(request, response)
470
+ end
471
+ end
472
+
473
+ # Write the set point for aliases on a device
474
+ # @param identifier [String] The identifier for the device to write.
475
+ # @param values [Hash] Aliases and the values to write.
476
+ def write(identifier, values)
477
+ debug "Will Write: #{values}"
478
+ patch("/#{identifier}/state", values)
479
+ end
480
+
481
+ # Read the current state for a device
482
+ # @param identifier [String] The identifier for the device to read.
483
+ def read(identifier)
484
+ get("/#{identifier}/state")
485
+ end
486
+ end
487
+ end
488
+ end
489
+