vines-services 0.1.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.
Files changed (69) hide show
  1. data/LICENSE +19 -0
  2. data/README +40 -0
  3. data/Rakefile +130 -0
  4. data/bin/vines-services +95 -0
  5. data/conf/config.rb +25 -0
  6. data/lib/vines/services/command/init.rb +209 -0
  7. data/lib/vines/services/command/restart.rb +14 -0
  8. data/lib/vines/services/command/start.rb +30 -0
  9. data/lib/vines/services/command/stop.rb +20 -0
  10. data/lib/vines/services/command/views.rb +26 -0
  11. data/lib/vines/services/component.rb +26 -0
  12. data/lib/vines/services/config.rb +105 -0
  13. data/lib/vines/services/connection.rb +120 -0
  14. data/lib/vines/services/controller/attributes_controller.rb +19 -0
  15. data/lib/vines/services/controller/base_controller.rb +99 -0
  16. data/lib/vines/services/controller/disco_info_controller.rb +61 -0
  17. data/lib/vines/services/controller/labels_controller.rb +17 -0
  18. data/lib/vines/services/controller/members_controller.rb +44 -0
  19. data/lib/vines/services/controller/messages_controller.rb +66 -0
  20. data/lib/vines/services/controller/probes_controller.rb +45 -0
  21. data/lib/vines/services/controller/services_controller.rb +81 -0
  22. data/lib/vines/services/controller/subscriptions_controller.rb +39 -0
  23. data/lib/vines/services/controller/systems_controller.rb +45 -0
  24. data/lib/vines/services/controller/transfers_controller.rb +58 -0
  25. data/lib/vines/services/controller/uploads_controller.rb +62 -0
  26. data/lib/vines/services/controller/users_controller.rb +127 -0
  27. data/lib/vines/services/core_ext/blather.rb +46 -0
  28. data/lib/vines/services/core_ext/couchrest.rb +33 -0
  29. data/lib/vines/services/indexer.rb +195 -0
  30. data/lib/vines/services/priority_queue.rb +94 -0
  31. data/lib/vines/services/roster.rb +70 -0
  32. data/lib/vines/services/storage/couchdb/fragment.rb +23 -0
  33. data/lib/vines/services/storage/couchdb/service.rb +170 -0
  34. data/lib/vines/services/storage/couchdb/system.rb +141 -0
  35. data/lib/vines/services/storage/couchdb/upload.rb +66 -0
  36. data/lib/vines/services/storage/couchdb/user.rb +137 -0
  37. data/lib/vines/services/storage/couchdb/vcard.rb +13 -0
  38. data/lib/vines/services/storage/couchdb.rb +157 -0
  39. data/lib/vines/services/storage.rb +33 -0
  40. data/lib/vines/services/throttle.rb +26 -0
  41. data/lib/vines/services/version.rb +7 -0
  42. data/lib/vines/services/vql/compiler.rb +94 -0
  43. data/lib/vines/services/vql/vql.citrus +115 -0
  44. data/lib/vines/services/vql/vql.rb +186 -0
  45. data/lib/vines/services.rb +71 -0
  46. data/test/config_test.rb +242 -0
  47. data/test/priority_queue_test.rb +23 -0
  48. data/test/storage/couchdb_test.rb +30 -0
  49. data/test/vql/compiler_test.rb +96 -0
  50. data/test/vql/vql_test.rb +233 -0
  51. data/web/coffeescripts/api.coffee +51 -0
  52. data/web/coffeescripts/commands.coffee +18 -0
  53. data/web/coffeescripts/files.coffee +315 -0
  54. data/web/coffeescripts/init.coffee +21 -0
  55. data/web/coffeescripts/services.coffee +356 -0
  56. data/web/coffeescripts/setup.coffee +503 -0
  57. data/web/coffeescripts/systems.coffee +371 -0
  58. data/web/images/default-service.png +0 -0
  59. data/web/images/linux.png +0 -0
  60. data/web/images/mac.png +0 -0
  61. data/web/images/run.png +0 -0
  62. data/web/images/windows.png +0 -0
  63. data/web/index.html +17 -0
  64. data/web/stylesheets/common.css +52 -0
  65. data/web/stylesheets/files.css +218 -0
  66. data/web/stylesheets/services.css +181 -0
  67. data/web/stylesheets/setup.css +117 -0
  68. data/web/stylesheets/systems.css +142 -0
  69. metadata +230 -0
@@ -0,0 +1,70 @@
1
+ # encoding: UTF-8
2
+ module Vines
3
+ class Storage
4
+ class CouchDB < Storage
5
+
6
+ ALL_SERVICES = '/_design/Service/_view/by_name'.freeze
7
+
8
+ # The existing storage class needs another method for our custom roster queries
9
+ # The default escaping in URLs makes this method necessary
10
+ def get_services
11
+ http = EM::HttpRequest.new("#{@url}#{ALL_SERVICES}").get
12
+ http.errback { yield }
13
+ http.callback do
14
+ doc = if http.response_header.status == 200
15
+ JSON.parse(http.response) rescue nil
16
+ end
17
+ yield doc
18
+ end
19
+ end
20
+
21
+ #In order to supply the correct vines roster logic, we need to over ride the default
22
+ #User creation of in the vines server. This method is much more effecient than
23
+ #looking up roster memberships each time the roster is sent.
24
+ def find_user(jid)
25
+ jid = JID.new(jid || '').bare.to_s
26
+ if jid.empty? then yield; return end
27
+ get("user:#{jid}") do |doc|
28
+ user = if doc && doc['type'] == 'User'
29
+ User.new(:jid => jid).tap do |user|
30
+ user.name, user.password = doc.values_at('name', 'password')
31
+ if doc['roster'] != ""
32
+ (doc['roster'] || {}).each_pair do |jid, props|
33
+ user.roster << Contact.new(
34
+ :jid => jid,
35
+ :name => props['name'],
36
+ :subscription => props['subscription'],
37
+ :ask => props['ask'],
38
+ :groups => props['groups'] || [])
39
+ end
40
+ end
41
+ add_user_roster_services(user)
42
+ end
43
+ end
44
+ yield user
45
+ end
46
+ end
47
+ fiber :find_user
48
+
49
+ # We will go find each service that contains this user jid in the
50
+ # users of the service document.
51
+ def add_user_roster_services(user)
52
+ self.get_services do |cdoc|
53
+ if cdoc
54
+ rows = cdoc['rows'].map do |row|
55
+ if row['value']['users'].include?(user.jid.to_s)
56
+ jid = JID.new("#{row['value']['jid']}").bare.to_s
57
+ user.roster << Contact.new(
58
+ :jid => jid,
59
+ :name => row['value']['name'],
60
+ :subscription => "both",
61
+ :ask => "subscribe",
62
+ :groups => ["Vines"])
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module CouchModels
6
+ class Fragment < CouchRest::Model::Base
7
+ extend Storage::CouchDB::ClassMethods
8
+
9
+ property :xml, String
10
+
11
+ design do
12
+ view :by_jid,
13
+ map: %q{
14
+ function(doc) {
15
+ if (doc.type != 'Fragment') return;
16
+ emit(doc['_id'].split(':')[1], null);
17
+ }
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,170 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module CouchModels
6
+ class Service < CouchRest::Model::Base
7
+ extend Storage::CouchDB::ClassMethods
8
+
9
+ KEYS = %w[_id name code accounts users jid created_at modified_at].freeze
10
+ VIEW_ID = "_design/System".freeze
11
+ VIEW_NAME = "System/memberships".freeze
12
+
13
+ attr_writer :size
14
+
15
+ after_save :update_views
16
+ after_destroy :update_views
17
+
18
+ property :name, String
19
+ property :code, String
20
+ property :accounts, [String], :default => []
21
+ property :users, [String], :default => []
22
+ property :jid, String
23
+
24
+ timestamps!
25
+
26
+ validates_uniqueness_of :name
27
+ validates_presence_of :name
28
+ validates_presence_of :code
29
+ validates_uniqueness_of :jid
30
+ validates_presence_of :jid
31
+ validate :compile_view
32
+
33
+ design do
34
+ view :by_name,
35
+ map: %q{
36
+ function(doc) {
37
+ if (doc.type != 'Service' || !doc.name) return;
38
+ emit(doc.name, doc);
39
+ }
40
+ }
41
+
42
+ view :by_jid,
43
+ map: %q{
44
+ function(doc) {
45
+ if (doc.type != 'Service' || !doc.jid) return;
46
+ emit(doc.jid, doc);
47
+ }
48
+ }
49
+
50
+ view :by_account,
51
+ map: %q{
52
+ function(doc) {
53
+ if (doc.type != 'Service' || !doc.accounts) return;
54
+ doc.accounts.forEach(function(account) {
55
+ emit(account, doc);
56
+ });
57
+ }
58
+ },
59
+ reduce: '_count'
60
+
61
+ view :by_user,
62
+ map: %q{
63
+ function(doc) {
64
+ if (doc.type != 'Service' || !doc.users) return;
65
+ doc.users.forEach(function(jid) {
66
+ emit(jid, doc);
67
+ });
68
+ }
69
+ },
70
+ reduce: '_count'
71
+ end
72
+
73
+ # Return true if this JID is allowed access to this service.
74
+ def user?(jid)
75
+ users.include?(jid.to_s.downcase)
76
+ end
77
+
78
+ # Allow the user, specified by their JID, to access the members of this
79
+ # service. Adds the JID to the list and ensures the list stays sorted
80
+ # and unique.
81
+ def add_user(jid)
82
+ users << jid.to_s.downcase
83
+ users.sort!
84
+ users.uniq!
85
+ end
86
+
87
+ # Remove this user's permission to access this service.
88
+ def remove_user(jid)
89
+ users.delete(jid.to_s.downcase)
90
+ end
91
+
92
+ # Return the number of members in this service's view. This is faster
93
+ # than calling Service#members#size because this reduces the view,
94
+ # so all members aren't loaded from the database. The size is cached so
95
+ # subsequent calls to this method do not query the view.
96
+ def size
97
+ unless @size
98
+ rows = database.view(VIEW_NAME, reduce: true, key: [0, id])['rows'] rescue []
99
+ @size = rows.first ? rows.first['value'] : 0
100
+ end
101
+ @size
102
+ end
103
+
104
+ # Query the members view and return an Array of Hashes like this:
105
+ # [{name: 'www.wonderland.lit', os: 'linux'}]. The members are cached
106
+ # so subsequent calls to this method do not query the view.
107
+ def members
108
+ unless @members
109
+ rows = database.view(VIEW_NAME, reduce: false, key: [0, id])['rows'] rescue []
110
+ @members = rows.map {|row| row['value'] }
111
+ end
112
+ @members
113
+ end
114
+
115
+ def to_result
116
+ to_hash.clone.keep_if {|k, v| KEYS.include?(k) }
117
+ .tap {|h| h['id'] = h.delete('_id') }
118
+ end
119
+
120
+ def self.find_by_name(name)
121
+ first_from_view('by_name', name)
122
+ end
123
+
124
+ def self.find_by_jid(jid)
125
+ first_from_view('by_jid', jid.to_s.downcase)
126
+ end
127
+
128
+ def self.find_by_user(jid)
129
+ by_user.key(jid.to_s.downcase).to_a
130
+ end
131
+
132
+ def self.find_all
133
+ sizes = find_sizes
134
+ by_name.map do |doc|
135
+ doc.size = sizes[doc.id] || 0
136
+ doc
137
+ end
138
+ end
139
+
140
+ # Return a Hash of service ID to member count.
141
+ def self.find_sizes
142
+ {}.tap do |hash|
143
+ rows = database.view(VIEW_NAME, reduce: true, group: true, startkey: [0], endkey: [1])['rows'] rescue []
144
+ rows.each do |row|
145
+ hash[row['key'][1]] = row['value']
146
+ end
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def compile_view
153
+ VQL::Compiler.new.to_js(code)
154
+ rescue Exception => e
155
+ errors.add(:base, e.message)
156
+ end
157
+
158
+ def update_views
159
+ js = VQL::Compiler.new.to_full_js(self.class.by_name.to_a)
160
+ design = database.get(VIEW_ID) rescue nil
161
+ design ||= {'_id' => VIEW_ID, 'views' => {}}
162
+ design['views']['memberships'] = {map: js, reduce: '_count'}
163
+ database.save_doc(design)
164
+ # trigger view update, discard results
165
+ EM::HttpRequest.new("#{database.root}/#{VIEW_ID}/_view/memberships").get
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,141 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module CouchModels
6
+ class System < CouchRest::Model::Base
7
+ extend Storage::CouchDB::ClassMethods
8
+
9
+ KEYS = %w[_id ohai created_at modified_at].freeze
10
+ VIEW_NAME = "System/memberships".freeze
11
+
12
+ attr_writer :services
13
+
14
+ property :ohai, Hash
15
+
16
+ timestamps!
17
+
18
+ validates_presence_of :ohai
19
+
20
+ design do
21
+ # System docs are large so don't include them in the view. Use
22
+ # include_docs=true when querying the view, if the full document
23
+ # is needed.
24
+ view :by_name,
25
+ map: %q{
26
+ function(doc) {
27
+ if (doc.type != 'System') return;
28
+ emit(doc['_id'].replace('system:', ''), null);
29
+ }
30
+ }
31
+
32
+ view :attributes,
33
+ map: %q{
34
+ function(doc) {
35
+ if (doc.type != 'System' || !doc.ohai) return;
36
+ Object.keys(doc.ohai).forEach(function(key) {
37
+ emit(key, null);
38
+ });
39
+ }
40
+ },
41
+ reduce: '_count'
42
+ end
43
+
44
+ def name
45
+ id.sub('system:', '')
46
+ end
47
+
48
+ # Query the members view and return the Service objects to which this
49
+ # System belongs. The services are cached so subsequent calls to this
50
+ # method do not query the view.
51
+ def services
52
+ unless @services
53
+ rows = database.view(VIEW_NAME, reduce: false, key: [1, name])['rows'] rescue []
54
+ ids = rows.map {|row| row['value'] }
55
+ @services = Service.all.keys(ids).to_a
56
+ end
57
+ @services
58
+ end
59
+
60
+ # Return a Hash of unix user ID to an Array of JID's allowed to access
61
+ # that account. For example:
62
+ # {'apache' => ['alice@wonderland.lit'],
63
+ # 'root' => ['hatter@wonderland.lit]}
64
+ def permissions
65
+ {}.tap do |perms|
66
+ services.each do |service|
67
+ service.accounts.each do |unix_id|
68
+ jids = (perms[unix_id] ||= [])
69
+ jids << service.users
70
+ jids.flatten!
71
+ jids.sort!.uniq!
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def to_result
78
+ to_hash.clone.keep_if {|k, v| KEYS.include?(k) }.tap do |h|
79
+ h['name'] = h.delete('_id').sub('system:', '')
80
+ h['services'] = services.map {|s| {id: s.id, jid: s.jid, name: s.name} }
81
+ h['permissions'] = permissions
82
+ end
83
+ end
84
+
85
+ # Send updated permissions to each system that belongs to the service
86
+ # (or belonged to it before the save).
87
+ def self.notify_members(stream, from, members)
88
+ return if members.empty?
89
+ names = members.map {|m| m['name'] }.uniq
90
+ systems = System.find_by_names(names)
91
+ nodes = systems.map do |system|
92
+ Blather::Stanza::Iq::Query.new(:set).tap do |result|
93
+ result.to = Blather::JID.new(system.name, from.domain)
94
+ result.query.content = system.to_result.to_json
95
+ result.query.namespace = 'http://getvines.com/protocol/systems'
96
+ end
97
+ end
98
+ Throttle.new(stream).async_send(nodes)
99
+ end
100
+
101
+ def self.find_attributes
102
+ view = attributes.reduce.group
103
+ view.rows.map {|row| row['key'] }
104
+ end
105
+
106
+ def self.find_all
107
+ by_name.rows.map do |row|
108
+ {name: row['key']}
109
+ end
110
+ end
111
+
112
+ def self.find_by_name(name)
113
+ find("system:#{name.downcase}")
114
+ end
115
+
116
+ # Return an Array of Systems with the given names. This method
117
+ # efficiently bulk loads systems and their services much more quickly
118
+ # than loading systems one by one. Note that the systems returned by
119
+ # this method do not have their ohai data loaded because it's expensive.
120
+ def self.find_by_names(names)
121
+ keys = names.map {|name| [1, name.downcase] }
122
+ rows = database.view(VIEW_NAME, reduce: false, keys: keys)['rows'] rescue []
123
+ ids = rows.map {|row| row['value'] }.uniq
124
+ services = Service.all.keys(ids).to_a
125
+ by_id = Hash[services.map {|s| [s.id, s] }]
126
+ by_name = Hash.new do |h, k|
127
+ h[k] = System.new(id: "system:#{k}").tap do |system|
128
+ system.services = []
129
+ end
130
+ end
131
+ rows.each do |row|
132
+ name = row['key'][1]
133
+ service = by_id[row['value']]
134
+ by_name[name].services << service
135
+ end
136
+ by_name.values
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module CouchModels
6
+ class Upload < CouchRest::Model::Base
7
+ extend Storage::CouchDB::ClassMethods
8
+
9
+ KEYS = %w[_id name size labels created_at modified_at].freeze
10
+
11
+ property :name, String
12
+ property :size, Integer
13
+ property :labels, [String], :default => []
14
+
15
+ timestamps!
16
+
17
+ validates_uniqueness_of :name
18
+ validates_presence_of :name
19
+ validates_numericality_of :size, only_integer: true, greater_than: -1
20
+
21
+ design do
22
+ view :by_name,
23
+ map: %q{
24
+ function(doc) {
25
+ if (doc.type != 'Upload' || !doc.name) return;
26
+ emit(doc.name, doc);
27
+ }
28
+ }
29
+
30
+ view :by_label,
31
+ map: %q{
32
+ function(doc) {
33
+ if (doc.type != 'Upload' || !doc.labels) return;
34
+ doc.labels.forEach(function(label) {
35
+ emit(label, doc);
36
+ });
37
+ }
38
+ },
39
+ reduce: '_count'
40
+ end
41
+
42
+ def to_result
43
+ to_hash.clone.keep_if {|k, v| KEYS.include?(k) }
44
+ .tap {|h| h['id'] = h.delete('_id') }
45
+ end
46
+
47
+ def self.find_labels
48
+ view = by_label.reduce.group
49
+ view.rows.map {|row| {name: row['key'], size: row['value']} }
50
+ end
51
+
52
+ def self.find_by_label(label)
53
+ by_label.key(label).map {|doc| doc.to_result }
54
+ end
55
+
56
+ def self.find_by_name(name)
57
+ first_from_view('by_name', name)
58
+ end
59
+
60
+ def self.find_all
61
+ by_name.map {|doc| doc.to_result }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,137 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module CouchModels
6
+ class User < CouchRest::Model::Base
7
+ extend Storage::CouchDB::ClassMethods
8
+
9
+ KEYS = %w[_id name permissions system created_at modified_at].freeze
10
+
11
+ before_save :enforce_constraints
12
+ after_destroy :remove_references
13
+
14
+ property :name, String
15
+ property :password, String
16
+ property :roster, Hash, :default => {}
17
+ property :permissions, Hash, :default => {}
18
+ property :system, TrueClass, :default => false
19
+
20
+ timestamps!
21
+
22
+ validates_presence_of :password
23
+
24
+ design do
25
+ view :by_jid,
26
+ map: %q{
27
+ function(doc) {
28
+ if (doc.type != 'User') return;
29
+ emit(doc['_id'].replace('user:', ''), doc);
30
+ }
31
+ }
32
+
33
+ view :subscribers,
34
+ map: %q{
35
+ function(doc) {
36
+ if (doc.type != 'User' || !doc.roster) return;
37
+ Object.keys(doc.roster).forEach(function(jid) {
38
+ emit(doc['_id'].replace('user:', ''), jid);
39
+ });
40
+ }
41
+ },
42
+ reduce: '_count'
43
+ end
44
+
45
+ %w[systems services users files].each do |name|
46
+ define_method "manage_#{name}?" do
47
+ !!read_attribute('permissions')[name]
48
+ end
49
+ define_method "manage_#{name}=" do |value|
50
+ read_attribute('permissions')[name] = !!value
51
+ end
52
+ end
53
+
54
+ def permissions=(perms)
55
+ perms ||= {}
56
+ self.manage_systems = perms['systems']
57
+ self.manage_services = perms['services']
58
+ self.manage_files = perms['files']
59
+ self.manage_users = perms['users']
60
+ end
61
+
62
+ def password=(desired)
63
+ desired = (desired || '').strip
64
+ raise 'password too short' if desired.size < (system ? 128 : 8)
65
+ write_attribute('password', BCrypt::Password.create(desired))
66
+ end
67
+
68
+ def change_password(previous, desired)
69
+ hash = BCrypt::Password.new(password) rescue nil
70
+ raise 'password failure' unless hash && hash == previous
71
+ self.password = desired
72
+ end
73
+
74
+ def jid
75
+ id ? id.sub('user:', '') : nil
76
+ end
77
+
78
+ # Query the Service/by_user view and return the Service objects to which
79
+ # this User has access. The services are cached so subsequent calls to
80
+ # this method do not query the view.
81
+ def services
82
+ @services ||= Service.find_by_user(jid)
83
+ end
84
+
85
+ def to_result
86
+ to_hash.clone.keep_if {|k, v| KEYS.include?(k) }.tap do |h|
87
+ h['jid'] = h.delete('_id').sub('user:', '')
88
+ h['services'] = h['system'] ? []: services.map {|s| s.id }
89
+ end
90
+ end
91
+
92
+ def self.find_all
93
+ by_jid.map do |doc|
94
+ {jid: doc.jid, name: doc.name, system: doc.system}
95
+ end
96
+ end
97
+
98
+ def self.find_by_jid(jid)
99
+ first_from_view('by_jid', jid.to_s.downcase)
100
+ end
101
+
102
+ private
103
+
104
+ # System users are not allowed to manage any other objects.
105
+ def enforce_constraints
106
+ if system
107
+ write_attribute('name', nil)
108
+ write_attribute('permissions', {})
109
+ end
110
+ end
111
+
112
+ # After the User document is deleted, remove references to the user from
113
+ # related documents (rosters, services, vcards and XML fragments).
114
+ def remove_references
115
+ if card = Vcard.find("vcard:#{jid}")
116
+ card.destroy
117
+ end
118
+
119
+ Fragment.by_jid.key(jid).each do |doc|
120
+ doc.destroy
121
+ end
122
+
123
+ Service.find_by_user(jid).each do |service|
124
+ service.remove_user(jid)
125
+ service.save
126
+ end
127
+
128
+ jids = User.subscribers.key(jid).rows.map {|row| row['value'] }
129
+ User.by_jid.keys(jids).each do |subscriber|
130
+ subscriber.roster.delete(jid)
131
+ subscriber.save
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module CouchModels
6
+ class Vcard < CouchRest::Model::Base
7
+ extend Storage::CouchDB::ClassMethods
8
+
9
+ property :card, String
10
+ end
11
+ end
12
+ end
13
+ end