MrMurano 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 :