vines-services 0.1.0

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