MrMurano 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 907d8f71d784965567eff90d5ca1d88e29e08b93
4
+ data.tar.gz: c97c0feb26e1f54898a8c24da2d1e84d30e7a6c0
5
+ SHA512:
6
+ metadata.gz: 436e65b69681a6df929bcbd8fe8a55842363dd6f13abae114d35209d59d83c575d082e8fd01737693d368a725defd8116b6c1aa59e638c5e566a6f39121bce4a
7
+ data.tar.gz: 639221f5cb46bea583e71d3945e6a3b74043548fef47dea5b234e7dae232ca1a02db0c264a31150130947fb4e5802e8bb581e648a549e8ac571add4e15a57c09
data/.gitignore ADDED
@@ -0,0 +1,30 @@
1
+ .DS_Store
2
+ .AppleDouble
3
+ .LSOverride
4
+ Icon
5
+ *.sw[a-z]
6
+ cookies
7
+ .jiraProject
8
+ .rpjProject
9
+ tags
10
+
11
+ xcuserdata
12
+ Pods/
13
+ pkg/
14
+
15
+ .mrmuranorc
16
+ files/
17
+ endpoints/
18
+ modules/
19
+ eventhandlers/
20
+ roles.yaml
21
+ users.yaml
22
+
23
+ # Thumbnails
24
+ ._*
25
+
26
+ # Files that might appear on external disk
27
+ .Spotlight-V100
28
+ .Trashes
29
+
30
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'http://rubygems.org'
2
+
3
+ #gemspec
4
+
5
+ gem 'commander', '~> 4.4.0'
6
+ gem 'http-form_data', '~> 1.0.1'
7
+ gem 'inifile', '~> 3.0'
8
+ gem 'netrc', '~> 0.11.0'
9
+
data/MrMurano.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
+ require 'mrmurano/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'MrMurano'
7
+ s.version = MrMurano::VERSION
8
+ s.authors = ['Michael Conrad Tadpol Tilstra']
9
+ s.email = ['tadpol@tadpol.org']
10
+ s.license = 'MIT'
11
+ s.homepage = 'https://github.com/tadpol/MrMurano'
12
+ s.summary = 'Do more from the command line with Murano'
13
+ s.description = %{Do more from the command line with Murano
14
+
15
+ Push and pull data from Murano.
16
+ Get status on what things have changed.
17
+ See a diff of the changes before you push.
18
+ }
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
23
+ s.require_paths = ['lib']
24
+
25
+ s.add_runtime_dependency('commander', '~> 4.4.0')
26
+ s.add_runtime_dependency('inifile', '~> 3.0')
27
+ s.add_runtime_dependency('netrc', '~> 0.11.0')
28
+ s.add_runtime_dependency('http-form_data', '~> 1.0.1')
29
+
30
+ s.add_development_dependency('bundler', '~> 1.7.6')
31
+ s.add_development_dependency('rspec', '~> 3.2')
32
+ s.add_development_dependency('rake')
33
+ end
34
+
35
+
data/README.markdown ADDED
@@ -0,0 +1,27 @@
1
+ # MrMurano
2
+
3
+ Do more from the command line with [Murano](https://exosite.com/platform/)
4
+
5
+ ## Usage
6
+
7
+ To start from an existing project in Murano
8
+ ```
9
+ mkdir myproject
10
+ cd myproject
11
+ mr syncdown --all
12
+ ```
13
+
14
+ Do stuff, see what changed: `mr status --all` or `mr diff -same`.
15
+ Then deploy `mr syncup --all`
16
+
17
+
18
+
19
+ ## Install
20
+
21
+ Source install for now.
22
+ ```
23
+ git clone … MrMurano
24
+ cd MrMurano
25
+ rake install
26
+ ```
27
+
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ #task :default => []
4
+
5
+ # TODO: figure out better way to test.
6
+ desc "Install gem in user dir"
7
+ task :bob do
8
+ sh %{gem install --user-install pkg/MrMurano-#{Bundler::GemHelper.gemspec.version}.gem}
9
+ end
10
+
11
+ desc "Uninstall from user dir"
12
+ task :unbob do
13
+ sh %{gem uninstall --user-install pkg/MrMurano-#{Bundler::GemHelper.gemspec.version}.gem}
14
+ end
15
+
16
+ task :echo do
17
+ puts "= #{Bundler::GemHelper.gemspec.version} ="
18
+ end
19
+
20
+ task :run do
21
+ sh %{ruby -Ilib bin/mr }
22
+ end
23
+
24
+ # vim: set sw=4 ts=4 :
25
+
data/TODO.taskpaper ADDED
@@ -0,0 +1,45 @@
1
+
2
+ Commands:
3
+ - Add Diff Command @done(2016-07-27)
4
+
5
+ Endpoints:
6
+ - Add support for multiple endpoints in one file
7
+ - Add directory support like in modules @done(2016-07-26)
8
+
9
+ Files:
10
+ - Fix upload. @done(2016-08-01)
11
+ - Files won't update, they always delete then add. @done(2016-07-28)
12
+
13
+ Users:
14
+ Much of this is stuck until we get more docs on the User/Role management
15
+ - Figure out how to upload (create and update) user info.
16
+ - Figure out how to add Roles to Users in the local data and upload it.
17
+ - Fix diff for Users and Roles.
18
+ - Have hash keys in the yaml be strings not symbols. (don't start with colon) @done(2016-07-27)
19
+
20
+ Product:
21
+ - Need to add way to set the product ID on a device eventhandler. @done(2016-08-01)
22
+
23
+ Config:
24
+ - Think about adding dev,staging,prod system; how would that work?
25
+
26
+ SolutionBase:
27
+ - All network traffic is serialized. Make some parallel.
28
+ - Rebuild how local names and paths are computed from remote items. @done(2016-07-27)
29
+
30
+ Bundles:
31
+ - Work on design
32
+ Thinking of something like VIM bundles. A directory of directories. Each with a
33
+ manafest file? (maybe) A Bundle is a group of modules, endpoints, static files
34
+ and the other things.
35
+
36
+ There needs to be some layering logic added, where the bundles are stacked and
37
+ then the top-level files are stack on top of that. This builds the final map of
38
+ what gets uploaded to the server.
39
+
40
+ For syncdown, bundles are considered to be read-only.
41
+
42
+ The goal is to have things like Users or Debug that you just include into a
43
+ project. And it gives all of the library, routes, statics and whatnot that you
44
+ need.
45
+
data/bin/mr ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'commander/import'
5
+ require 'pathname'
6
+ require 'MrMurano'
7
+ require 'pp'
8
+
9
+ program :version, MrMurano::VERSION
10
+ program :description, %{Manage a Solution in Exosite's Murano}
11
+
12
+ global_option('-V', '--verbose', 'Be chatty') {
13
+ $cfg['tool.verbose'] = true
14
+ }
15
+ global_option('-n', '--dry', %{Don't run actions that make changes}) {
16
+ $cfg['tool.dry'] = true
17
+ $cfg['tool.verbose'] = true # dry implies verbose
18
+ }
19
+ global_option '--skip-plugins', %{Don't load plugins. Good for when one goes bad.}
20
+
21
+ global_option('-C', '--configfile FILE', %{Load additional configuration file}) {|file|
22
+ # this is called after all of the top level code in this file.
23
+ $cfg.load_specific(file)
24
+ }
25
+ global_option('-c', '--config KEY=VALUE', %{Set a single config key}) {|param|
26
+ key, value = param.split('=', 2)
27
+ raise "Bad config '#{param}'" if key.nil?
28
+ $cfg[key] = value
29
+ }
30
+
31
+ default_command :help
32
+ #default_command :syncup
33
+
34
+ $cfg = MrMurano::Config.new
35
+ $cfg.load
36
+
37
+ # Basic command support is:
38
+ # - read/write config file in [Project, User, System] (all are optional)
39
+ # - Introspection for tab completion.
40
+ # - Look for tools in PATH that are +x and "mr-foo..."
41
+
42
+
43
+ # Look for plug-ins
44
+ pgds = [
45
+ Pathname.new(Dir.home) + '.mrmurano' + 'plugins'
46
+ ]
47
+ # Add plugin dirs from configs
48
+ # This is run before the command line options are parsed, so need to check old way.
49
+ if not ARGV.include? '--skip-plugins' then
50
+ pgds << Pathname.new(ENV['MR_MURANO_PLUGIN_DIR']) if ENV.has_key? 'MR_MURANO_PLUGIN_DIR'
51
+ pgds.each do |path|
52
+ next unless path.exist?
53
+ path.each_child do |plugin|
54
+ next if plugin.directory?
55
+ next unless plugin.readable?
56
+ next if plugin.basename.fnmatch('.*') # don't read anything starting with .
57
+ begin
58
+ require plugin.to_s
59
+ rescue Exception => e
60
+ $stderr.puts "Failed to load plugin at #{plugin} because #{e}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,176 @@
1
+ require 'netrc'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'date'
6
+ require 'pp'
7
+ require 'terminal-table'
8
+
9
+ module MrMurano
10
+ class Account
11
+
12
+
13
+ def endPoint(path)
14
+ URI('https://' + $cfg['net.host'] + '/api:1/' + path.to_s)
15
+ end
16
+
17
+ def _password
18
+ host = $cfg['net.host']
19
+ user = $cfg['user.name']
20
+ if user.nil? then
21
+ user = ask("Account name: ")
22
+ $cfg['user.name'] = user
23
+ end
24
+ # Maybe in the future use Keychain. For now all in Netrc.
25
+ # if (/darwin/ =~ RUBY_PLATFORM) != nil then
26
+ # # macOS
27
+ # pws = `security 2>&1 >/dev/null find-internet-password -gs "#{host}" -a "#{user}"`
28
+ # pws.strip!
29
+ # pws.sub!(/^password: "(.*)"$/, '\1')
30
+ # return pws
31
+ # Use Netrc
32
+ nrc = Netrc.read
33
+ ruser, pws = nrc[host]
34
+ pws = nil unless ruser == user
35
+ if pws.nil? then
36
+ pws = ask("Password: ") { |q| q.echo = "*" }
37
+ nrc[host] = user, pws
38
+ nrc.save
39
+ end
40
+ pws
41
+ end
42
+
43
+ def token
44
+ if @token.nil? then
45
+ r = endPoint('token/')
46
+ Net::HTTP.start(r.host, r.port, :use_ssl=>true) do |http|
47
+ request = Net::HTTP::Post.new(r)
48
+ request.content_type = 'application/json'
49
+ #request.basic_auth(username(), password())
50
+ request.body = JSON.generate({
51
+ :email => $cfg['user.name'],
52
+ :password => _password
53
+ })
54
+
55
+ response = http.request(request)
56
+ case response
57
+ when Net::HTTPSuccess
58
+ token = JSON.parse(response.body)
59
+ @token = token['token']
60
+ else
61
+ say_error "No token! because: #{response}"
62
+ @token = nil
63
+ raise response
64
+ end
65
+ end
66
+ end
67
+ @token
68
+ end
69
+
70
+ def businesses
71
+ r = endPoint('user/' + $cfg['user.name'] + '/membership/')
72
+ Net::HTTP.start(r.host, r.port, :use_ssl=>true) do |http|
73
+ request = Net::HTTP::Get.new(r)
74
+ request.content_type = 'application/json'
75
+ request['authorization'] = 'token ' + token
76
+
77
+ response = http.request(request)
78
+ case response
79
+ when Net::HTTPSuccess
80
+ busy = JSON.parse(response.body)
81
+ return busy
82
+ else
83
+ raise response
84
+ end
85
+ end
86
+ end
87
+
88
+ def products
89
+ r = endPoint('business/' + $cfg['business.id'] + '/product/')
90
+ Net::HTTP.start(r.host, r.port, :use_ssl=>true) do |http|
91
+ request = Net::HTTP::Get.new(r)
92
+ request.content_type = 'application/json'
93
+ request['authorization'] = 'token ' + token
94
+
95
+ response = http.request(request)
96
+ case response
97
+ when Net::HTTPSuccess
98
+ busy = JSON.parse(response.body)
99
+ return busy
100
+ else
101
+ raise response
102
+ end
103
+ end
104
+ end
105
+
106
+ def solutions
107
+ r = endPoint('business/' + $cfg['business.id'] + '/solution/')
108
+ Net::HTTP.start(r.host, r.port, :use_ssl=>true) do |http|
109
+ request = Net::HTTP::Get.new(r)
110
+ request.content_type = 'application/json'
111
+ request['authorization'] = 'token ' + token
112
+
113
+ response = http.request(request)
114
+ case response
115
+ when Net::HTTPSuccess
116
+ busy = JSON.parse(response.body)
117
+ return busy
118
+ else
119
+ raise response
120
+ end
121
+ end
122
+ end
123
+
124
+ end
125
+ end
126
+
127
+ # This is largely for testing.
128
+ command :account do |c|
129
+ c.syntax = %{mr account ...}
130
+ c.description = %{Show things about your account.}
131
+ c.option '--businesses', 'Get businesses for user'
132
+ c.option '--products', 'Get products for user (needs a business)'
133
+ c.option '--solutions', 'Get solutions for user (needs a business)'
134
+ c.option '--idonly', 'Only return the ids'
135
+
136
+ c.action do |args, options|
137
+
138
+ acc = MrMurano::Account.new
139
+
140
+ if options.businesses then
141
+ data = acc.businesses
142
+ if options.idonly then
143
+ say data.map{|row| row['bizid']}.join(' ')
144
+ else
145
+ busy = data.map{|row| [row['bizid'], row['role'], row['name']]}
146
+ table = Terminal::Table.new :rows => busy, :headings => ['Biz ID', 'Role', 'Name']
147
+ say table
148
+ end
149
+
150
+ elsif options.products then
151
+ data = acc.products
152
+ if options.idonly then
153
+ say data.map{|row| row['pid']}.join(' ')
154
+ else
155
+ busy = data.map{|r| [r['label'], r['type'], r['pid'], r['modelId']]}
156
+ table = Terminal::Table.new :rows => busy, :headings => ['Label', 'Type', 'PID', 'ModelID']
157
+ say table
158
+ end
159
+
160
+ elsif options.solutions then
161
+ data = acc.solutions
162
+ if options.idonly then
163
+ say data.map{|row| row['apiId']}.join(' ')
164
+ else
165
+ busy = data.map{|r| [r['apiId'], r['domain'], r['type'], r['sid']]}
166
+ table = Terminal::Table.new :rows => busy, :headings => ['API ID', 'Domain', 'Type', 'SID']
167
+ say table
168
+ end
169
+
170
+ else
171
+ say acc.token
172
+ end
173
+
174
+ end
175
+ end
176
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,107 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'pp'
5
+
6
+ module MrMurano
7
+ # …/endpoint
8
+ class Endpoint < SolutionBase
9
+ def initialize
10
+ super
11
+ @uriparts << 'endpoint'
12
+ end
13
+
14
+ ##
15
+ # This gets all data about all endpoints
16
+ def list
17
+ get()
18
+ end
19
+
20
+ def fetch(id)
21
+ ret = get('/' + id.to_s)
22
+ aheader = ret['script'].lines.first.chomp
23
+ dheader = /^--#ENDPOINT (?i:#{ret['method']}) #{ret['path']}$/
24
+ if block_given? then
25
+ yield dheader + "\n" unless dheader =~ aheader
26
+ yield ret['script']
27
+ else
28
+ res = ''
29
+ res << dheader + "\n" unless dheader =~ aheader
30
+ res << ret['script']
31
+ res
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Upload endpoint
37
+ # :local path to file to push
38
+ # :remote hash of method and endpoint path
39
+ def upload(local, remote)
40
+ local = Pathname.new(local) unless local.kind_of? Pathname
41
+ raise "no file" unless local.exist?
42
+
43
+ # we assume these are small enough to slurp.
44
+ script = local.read
45
+ limitkeys = [:method, :path, :script, @itemkey]
46
+ remote = remote.select{|k,v| limitkeys.include? k }
47
+ remote[:script] = script
48
+ # post('', remote)
49
+ if remote.has_key? @itemkey then
50
+ put('/' + remote[@itemkey], remote) do |request, http|
51
+ response = http.request(request)
52
+ case response
53
+ when Net::HTTPSuccess
54
+ #return JSON.parse(response.body)
55
+ when Net::HTTPNotFound
56
+ verbose "\tDoesn't exist, creating"
57
+ post('/', remote)
58
+ else
59
+ say_error "got #{response} from #{request} #{request.uri.to_s}"
60
+ say_error ":: #{response.body}"
61
+ end
62
+ end
63
+ else
64
+ verbose "\tNo itemkey, creating"
65
+ post('/', remote)
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Delete an endpoint
71
+ def remove(id)
72
+ delete('/' + id.to_s)
73
+ end
74
+
75
+ def tolocalname(item, key)
76
+ name = item[:method].downcase
77
+ name << '_'
78
+ name << item[:path].gsub(/\//, '-')
79
+ name << '.lua'
80
+ end
81
+
82
+ def toremotename(from, path)
83
+ # read first line of file and get method/path from it?
84
+ path = Pathname.new(path) unless path.kind_of? Pathname
85
+ aheader = path.readlines().first
86
+ md = /--#ENDPOINT (\S+) (.*)/.match(aheader)
87
+ raise "Not an Endpoint: #{path.to_s}" if md.nil?
88
+ {:method=>md[1], :path=>md[2]}
89
+ end
90
+
91
+ def synckey(item)
92
+ "#{item[:method].upcase}_#{item[:path]}"
93
+ end
94
+
95
+ def docmp(itemA, itemB)
96
+ if itemA[:script].nil? and itemA[:local_path] then
97
+ itemA[:script] = itemA[:local_path].read
98
+ end
99
+ if itemB[:script].nil? and itemB[:local_path] then
100
+ itemB[:script] = itemB[:local_path].read
101
+ end
102
+ return itemA[:script] != itemB[:script]
103
+ end
104
+
105
+ end
106
+ end
107
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,137 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require "http/form_data"
4
+ require 'digest/sha1'
5
+ require 'mime/types'
6
+ require 'pp'
7
+
8
+ module MrMurano
9
+ # …/file
10
+ class File < SolutionBase
11
+ def initialize
12
+ super
13
+ @uriparts << 'file'
14
+ @itemkey = :path
15
+ end
16
+
17
+ ##
18
+ # Get a list of all of the static content
19
+ def list
20
+ get()
21
+ end
22
+
23
+ ##
24
+ # Get one item of the static content.
25
+ def fetch(path, &block)
26
+ get(path) do |request, http|
27
+ http.request(request) do |resp|
28
+ case resp
29
+ when Net::HTTPSuccess
30
+ if block_given? then
31
+ resp.read_body &block
32
+ else
33
+ resp.read_body do |chunk|
34
+ $stdout.write chunk
35
+ end
36
+ end
37
+ else
38
+ say_error "got #{resp.to_s} from #{request} #{request.uri.to_s}"
39
+ raise resp
40
+ end
41
+ end
42
+ nil
43
+ end
44
+ end
45
+
46
+ ##
47
+ # Delete a file
48
+ def remove(path)
49
+ # TODO test
50
+ delete('/'+path)
51
+ end
52
+
53
+ ##
54
+ # Upload a file
55
+ def upload(local, remote)
56
+ local = Pathname.new(local) unless local.kind_of? Pathname
57
+
58
+ uri = endPoint('upload' + remote[:path])
59
+ # kludge past for a bit.
60
+ #`curl -s -H 'Authorization: token #{@token}' '#{uri.to_s}' -F file=@#{local.to_s}`
61
+
62
+ # http://stackoverflow.com/questions/184178/ruby-how-to-post-a-file-via-http-as-multipart-form-data
63
+ #
64
+ # Look at: https://github.com/httprb/http
65
+ # If it works well, consider porting over to it.
66
+ #
67
+ # Or just: https://github.com/httprb/form_data.rb ?
68
+ #
69
+ # Most of these pull into ram. So maybe just go with that. Would guess that
70
+ # truely large static content is rare, and we can optimize/fix that later.
71
+
72
+ form = HTTP::FormData.create(:file=>HTTP::FormData::File.new(local.to_s))
73
+ req = Net::HTTP::Put.new(uri)
74
+ workit(req) do |request,http|
75
+ request.content_type = form.content_type
76
+ request.content_length = form.content_length
77
+ request.body = form.to_s
78
+
79
+ response = http.request(request)
80
+ case response
81
+ when Net::HTTPSuccess
82
+ else
83
+ say_error "got #{response} from #{request} #{request.uri.to_s}"
84
+ say_error ":: #{response.body}"
85
+ end
86
+ end
87
+ end
88
+
89
+ def tolocalname(item, key)
90
+ name = item[key]
91
+ name = $cfg['files.default_page'] if name == '/'
92
+ name
93
+ end
94
+
95
+ def toremotename(from, path)
96
+ name = super(from, path)
97
+ name = '/' if name == $cfg['files.default_page']
98
+ name = "/#{name}" unless name.chars.first == '/'
99
+
100
+ mime = MIME::Types.type_for(path.to_s)[0] || MIME::Types["application/octet-stream"][0]
101
+
102
+ sha1 = Digest::SHA1.file(path.to_s).hexdigest
103
+
104
+ {:path=>name, :mime_type=>mime.simplified, :checksum=>sha1}
105
+ end
106
+
107
+ def synckey(item)
108
+ item[:path]
109
+ end
110
+
111
+ def docmp(itemA, itemB)
112
+ return (itemA[:mime_type] != itemB[:mime_type] or
113
+ itemA[:checksum] != itemB[:checksum])
114
+ end
115
+
116
+ def locallist(from)
117
+ from = Pathname.new(from) unless from.kind_of? Pathname
118
+ unless from.exist? then
119
+ return []
120
+ end
121
+ raise "Not a directory: #{from.to_s}" unless from.directory?
122
+
123
+ Pathname.glob(from.to_s + '/**/*').map do |path|
124
+ name = toremotename(from, path)
125
+ case name
126
+ when Hash
127
+ name[:local_path] = path
128
+ name
129
+ else
130
+ {:local_path => path, :name => name}
131
+ end
132
+ end
133
+ end
134
+
135
+ end
136
+ end
137
+ # vim: set ai et sw=2 ts=2 :