myer 0.0.1

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +9 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +21 -0
  7. data/README.md +5 -0
  8. data/Rakefile +44 -0
  9. data/bin/myer +198 -0
  10. data/features/myer.feature +8 -0
  11. data/features/step_definitions/myer_steps.rb +6 -0
  12. data/features/support/env.rb +15 -0
  13. data/lib/myer.rb +25 -0
  14. data/lib/myer/admin_cli_controller.rb +70 -0
  15. data/lib/myer/api.rb +104 -0
  16. data/lib/myer/cli_controller.rb +204 -0
  17. data/lib/myer/config.rb +86 -0
  18. data/lib/myer/content.rb +92 -0
  19. data/lib/myer/crypto.rb +40 -0
  20. data/lib/myer/exceptions.rb +7 -0
  21. data/lib/myer/plot.rb +11 -0
  22. data/lib/myer/proc_net_parser.rb +11 -0
  23. data/lib/myer/test_cli_controller.rb +52 -0
  24. data/lib/myer/ticket.rb +3 -0
  25. data/lib/myer/ticket_store.rb +61 -0
  26. data/lib/myer/version.rb +3 -0
  27. data/myer.gemspec +26 -0
  28. data/myer.rdoc +5 -0
  29. data/scripts/plot-helper.py +32 -0
  30. data/spec/admin_cli_controller_spec.rb +128 -0
  31. data/spec/api_spec.rb +136 -0
  32. data/spec/cli_controller_spec.rb +368 -0
  33. data/spec/config_spec.rb +116 -0
  34. data/spec/content_spec.rb +103 -0
  35. data/spec/crypto_spec.rb +37 -0
  36. data/spec/data/myer-full.config +9 -0
  37. data/spec/data/myer-multiple.config +14 -0
  38. data/spec/data/myer.config +6 -0
  39. data/spec/data/plot-data.csv +31 -0
  40. data/spec/data/secret-ticket-12345678.json +1 -0
  41. data/spec/data/secret-ticket-987654321.json +1 -0
  42. data/spec/plot_spec.rb +9 -0
  43. data/spec/proc_net_parser_spec.rb +15 -0
  44. data/spec/shared_examples_for_config.rb +47 -0
  45. data/spec/spec_helper.rb +14 -0
  46. data/spec/test_cli_controller_spec.rb +72 -0
  47. data/spec/ticket_store_spec.rb +86 -0
  48. data/test/default_test.rb +14 -0
  49. data/test/test_helper.rb +9 -0
  50. metadata +220 -0
@@ -0,0 +1,204 @@
1
+ class CliController
2
+ include Myer::Config
3
+
4
+ attr_accessor :out, :crypto
5
+
6
+ def initialize
7
+ @out = STDOUT
8
+ initialize_config
9
+ @crypto = Crypto.new
10
+ end
11
+
12
+ def api(server_name = default_server)
13
+ api = MySelf::Api.new
14
+ api.server = server_name
15
+ if server(server_name)
16
+ api.user = server(server_name).user_id
17
+ api.password = server(server_name).user_password
18
+ end
19
+ api
20
+ end
21
+
22
+ def create_bucket(name)
23
+ read_config
24
+
25
+ bucket_id = api.create_bucket
26
+
27
+ self.default_bucket_id = bucket_id
28
+
29
+ ticket = Ticket.new
30
+ ticket.server = default_server
31
+ ticket.bucket_id = bucket_id
32
+ ticket.key = @crypto.generate_passphrase
33
+ ticket.name = name
34
+
35
+ store = TicketStore.new(config_dir)
36
+ store.save_ticket(ticket)
37
+
38
+ write_config
39
+
40
+ out.puts("Created new bucket and stored its secret ticket at #{store.ticket_path(ticket)}.")
41
+ out.puts("You need this ticket to give other clients access to the bucket.")
42
+ out.puts("Keep it safe and secret. Everybody who has the ticket can read the bucket data.")
43
+
44
+ bucket_id
45
+ end
46
+
47
+ def create_token
48
+ read_config
49
+
50
+ token = api.create_token
51
+
52
+ out.puts("Created token: #{token}")
53
+ out.puts("Use this token to register another client, " +
54
+ "e.g. with `myer register #{default_server} #{token}`.")
55
+
56
+ token
57
+ end
58
+
59
+ def register(server_name, token)
60
+ read_config
61
+
62
+ self.default_server = server_name
63
+ self.user_id, self.user_password = api(server_name).register(token)
64
+
65
+ write_config
66
+ end
67
+
68
+ def write_item(bucket_id, content)
69
+ read_config
70
+
71
+ return api.create_item(bucket_id, content)
72
+ end
73
+
74
+ def write_raw(content)
75
+ read_config
76
+ write_item(default_bucket_id, content)
77
+ end
78
+
79
+ def write(content)
80
+ read_config
81
+
82
+ store = TicketStore.new(config_dir)
83
+ ticket = store.load_ticket(default_bucket_id)
84
+
85
+ @crypto.passphrase = ticket.key
86
+
87
+ encrypted_content = @crypto.encrypt(content)
88
+
89
+ write_item(default_bucket_id, encrypted_content)
90
+ end
91
+
92
+ def read_items(bucket_id)
93
+ read_config
94
+
95
+ store = TicketStore.new(config_dir)
96
+ ticket = store.load_ticket(default_bucket_id)
97
+
98
+ @crypto.passphrase = ticket.key
99
+
100
+ items = api.get_items(bucket_id)
101
+
102
+ content_list = []
103
+ items.each do |item|
104
+ content = @crypto.decrypt(item.content)
105
+ out.puts("#{item.id}: #{content}")
106
+ content_list.push(content)
107
+ end
108
+
109
+ content_list
110
+ end
111
+
112
+ def read
113
+ read_config
114
+ if !default_bucket_id || default_bucket_id.empty?
115
+ raise Myer::Error.new("Default bucket id not set")
116
+ end
117
+ inner_items = read_items(default_bucket_id)
118
+
119
+ FileUtils.mkdir_p(data_dir)
120
+ csv_file = local_csv_path(default_bucket_id)
121
+ content = Content.new
122
+
123
+ inner_items.each do |inner_item|
124
+ content.add(inner_item)
125
+ end
126
+
127
+ content.write_as_csv(csv_file)
128
+
129
+ inner_items
130
+ end
131
+
132
+ def create_payload(value, tag = nil)
133
+ payload = {}
134
+ payload["id"] = SecureRandom.hex
135
+ payload["written_at"] = Time.now.utc.strftime("%FT%TZ")
136
+ payload["tag"] = tag if tag
137
+ payload["data"] = value.to_s
138
+ JSON.generate(payload)
139
+ end
140
+
141
+ def write_value(value, tag = nil)
142
+ write(create_payload(value, tag))
143
+ end
144
+
145
+ def write_pair(value1, value2)
146
+ json = [ value1, value2 ]
147
+ write_value(JSON.generate(json))
148
+ end
149
+
150
+ def plot(dont_sync: false)
151
+ read_config
152
+
153
+ read unless dont_sync
154
+
155
+ plot = Plot.new
156
+ plot.show(local_csv_path(default_bucket_id))
157
+ end
158
+
159
+ def export(output_path)
160
+ read_config
161
+
162
+ content = Content.new
163
+ inner_items = read
164
+ inner_items.each do |inner_item|
165
+ content.add(inner_item)
166
+ end
167
+
168
+ content.write_as_json(output_path)
169
+ end
170
+
171
+ def consume_ticket(ticket_source_path)
172
+ read_config
173
+
174
+ ticket_target_path = File.join(config_dir, File.basename(ticket_source_path))
175
+ FileUtils.mv(ticket_source_path, ticket_target_path)
176
+ store = TicketStore.new(config_dir)
177
+ ticket = store.load_ticket_from_file(ticket_target_path)
178
+ self.default_bucket_id = ticket.bucket_id
179
+
180
+ write_config
181
+ end
182
+
183
+ def list_tickets(show_status: false)
184
+ read_config
185
+
186
+ store = TicketStore.new(config_dir)
187
+ out.puts "Available Tickets:"
188
+ store.tickets_per_server.each do |server_name,tickets|
189
+ if show_status
190
+ server_api = api(server_name)
191
+ begin
192
+ server_api.ping
193
+ status = " [pings]"
194
+ rescue StandardError => e
195
+ status = " [ping error: #{e.message.chomp}]"
196
+ end
197
+ end
198
+ out.puts " Server '#{server_name}'#{status}:"
199
+ tickets.each do |ticket|
200
+ out.puts " Bucket '#{ticket.name}' (#{ticket.bucket_id})"
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,86 @@
1
+ module Myer
2
+ module Config
3
+ include XDG::BaseDir::Mixin
4
+
5
+ def subdirectory
6
+ "myer"
7
+ end
8
+
9
+ attr_accessor :config_dir, :data_dir
10
+
11
+ def initialize_config
12
+ @config_dir = config.home.to_s
13
+ @data_dir = data.home.to_s
14
+ end
15
+
16
+ def local_csv_path(bucket_id)
17
+ File.join(@data_dir, bucket_id + ".csv")
18
+ end
19
+
20
+ class ServerConfig
21
+ attr_accessor :admin_id, :admin_password
22
+ attr_accessor :user_id, :user_password
23
+ attr_accessor :default_bucket_id
24
+
25
+ def initialize(yaml)
26
+ @admin_id = yaml["admin_id"]
27
+ @admin_password = yaml["admin_password"]
28
+ @user_id = yaml["user_id"]
29
+ @user_password = yaml["user_password"]
30
+ @default_bucket_id = yaml["default_bucket_id"]
31
+ end
32
+ end
33
+
34
+ def default_server=(value)
35
+ @config ||= {}
36
+ @config["default_server"] = value
37
+ end
38
+
39
+ def default_server
40
+ @config["default_server"]
41
+ end
42
+
43
+ def self.define_attribute(name)
44
+ define_method(name.to_s) do
45
+ return nil if !@config
46
+ @config["servers"][default_server][name.to_s]
47
+ end
48
+
49
+ define_method(name.to_s + "=") do |value|
50
+ @config ||= {}
51
+ @config["servers"] ||= {}
52
+ @config["servers"][default_server] ||= {}
53
+ @config["servers"][default_server][name.to_s] = value
54
+ end
55
+ end
56
+
57
+ define_attribute :admin_id
58
+ define_attribute :admin_password
59
+ define_attribute :user_id
60
+ define_attribute :user_password
61
+ define_attribute :default_bucket_id
62
+
63
+ def write_config
64
+ FileUtils.mkdir_p(@config_dir)
65
+ File.write(File.join(@config_dir, "myer.config"), @config.to_yaml)
66
+ end
67
+
68
+ def read_config
69
+ config_file = File.join(@config_dir, "myer.config")
70
+ return if !File.exist?(config_file)
71
+
72
+ @config = YAML.load_file(config_file)
73
+
74
+ @default_server = @config["default_server"]
75
+ end
76
+
77
+ def servers
78
+ @config["servers"].keys
79
+ end
80
+
81
+ def server(name)
82
+ return nil if !@config["servers"] || !@config["servers"].has_key?(name)
83
+ ServerConfig.new(@config["servers"][name])
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,92 @@
1
+ class Content
2
+ class Item
3
+ attr_accessor :id, :written_at, :tag
4
+
5
+ def initialize(container)
6
+ @container = container
7
+ end
8
+
9
+ def data=(value)
10
+ @data = value
11
+ end
12
+
13
+ def data
14
+ if @container.type == "json"
15
+ JSON.parse(@data)
16
+ else
17
+ @data
18
+ end
19
+ end
20
+ end
21
+
22
+ attr_reader :title, :type
23
+
24
+ def initialize
25
+ @items = []
26
+ end
27
+
28
+ def add(content)
29
+ json = JSON.parse(content)
30
+
31
+ item = Item.new(self)
32
+ item.id = json["id"]
33
+ item.written_at = json["written_at"]
34
+ item.tag = json["tag"]
35
+ if item.tag == "title"
36
+ @title = json["data"]
37
+ elsif item.tag == "type"
38
+ @type = json["data"]
39
+ else
40
+ item.data = json["data"]
41
+ @items.push(item)
42
+ end
43
+ end
44
+
45
+ def first
46
+ at(0)
47
+ end
48
+
49
+ def at(index)
50
+ @items.at(index)
51
+ end
52
+
53
+ def empty?
54
+ @items.empty?
55
+ end
56
+
57
+ def length
58
+ @items.length
59
+ end
60
+
61
+ def write_as_csv(output_path)
62
+ File.open(output_path, "w") do |file|
63
+ @items.each do |item|
64
+ if type == "json"
65
+ file.puts(item.data.join(","))
66
+ else
67
+ file.puts(item.data)
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def write_as_json(output_path)
74
+ json = {}
75
+ json["title"] = title
76
+
77
+ data_array = []
78
+ @items.each do |item|
79
+ data_item = {}
80
+ data_item["date"] = item.data[0]
81
+ data_item["value"] = item.data[1]
82
+
83
+ data_array.push(data_item)
84
+ end
85
+
86
+ json["data"] = data_array
87
+
88
+ File.open(output_path, "w") do |file|
89
+ file.write(json.to_json)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,40 @@
1
+ class Crypto
2
+
3
+ attr_accessor :passphrase
4
+
5
+ def generate_passphrase
6
+ `gpg --armor --gen-random 1 16`.chomp
7
+ end
8
+
9
+ def call_cmd(cmd, input)
10
+ output = nil
11
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
12
+ stdin.puts(input)
13
+ stdin.close
14
+ output = stdout.read
15
+
16
+ if !wait_thr.value.success?
17
+ raise Myer::CmdFailed.new(stderr.read)
18
+ end
19
+ end
20
+ output
21
+ end
22
+
23
+ def encrypt(plaintext)
24
+ cmd = "gpg --batch --armor --passphrase '#{passphrase}' --symmetric"
25
+ begin
26
+ return call_cmd(cmd, plaintext)
27
+ rescue Myer::CmdFailed => e
28
+ raise "Encryption failed: #{e}"
29
+ end
30
+ end
31
+
32
+ def decrypt(ciphertext)
33
+ cmd = "gpg --batch --passphrase '#{passphrase}' --decrypt"
34
+ begin
35
+ return call_cmd(cmd, ciphertext).chomp
36
+ rescue Myer::CmdFailed => e
37
+ raise Myer::DecryptionFailed.new("Decryption failed: #{e}")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ module Myer
2
+ class Error < StandardError; end
3
+
4
+ class CmdFailed < Error; end
5
+ class DecryptionFailed < Error; end
6
+ end
7
+
@@ -0,0 +1,11 @@
1
+ class Plot
2
+ def call_helper(csv_file_path)
3
+ plot_helper = File.expand_path( "../../../scripts/plot-helper.py", __FILE__ )
4
+ cmd = "python #{plot_helper} #{csv_file_path}"
5
+ system(cmd)
6
+ end
7
+
8
+ def show(csv_file)
9
+ call_helper(csv_file)
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class ProcNetParser
2
+ attr_reader :received_bytes
3
+
4
+ def parse(input)
5
+ input.each_line do |line|
6
+ if line =~ /^en(.*):\s+(\d+)\s+/
7
+ @received_bytes = $2.to_i
8
+ end
9
+ end
10
+ end
11
+ end