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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +5 -0
- data/Rakefile +44 -0
- data/bin/myer +198 -0
- data/features/myer.feature +8 -0
- data/features/step_definitions/myer_steps.rb +6 -0
- data/features/support/env.rb +15 -0
- data/lib/myer.rb +25 -0
- data/lib/myer/admin_cli_controller.rb +70 -0
- data/lib/myer/api.rb +104 -0
- data/lib/myer/cli_controller.rb +204 -0
- data/lib/myer/config.rb +86 -0
- data/lib/myer/content.rb +92 -0
- data/lib/myer/crypto.rb +40 -0
- data/lib/myer/exceptions.rb +7 -0
- data/lib/myer/plot.rb +11 -0
- data/lib/myer/proc_net_parser.rb +11 -0
- data/lib/myer/test_cli_controller.rb +52 -0
- data/lib/myer/ticket.rb +3 -0
- data/lib/myer/ticket_store.rb +61 -0
- data/lib/myer/version.rb +3 -0
- data/myer.gemspec +26 -0
- data/myer.rdoc +5 -0
- data/scripts/plot-helper.py +32 -0
- data/spec/admin_cli_controller_spec.rb +128 -0
- data/spec/api_spec.rb +136 -0
- data/spec/cli_controller_spec.rb +368 -0
- data/spec/config_spec.rb +116 -0
- data/spec/content_spec.rb +103 -0
- data/spec/crypto_spec.rb +37 -0
- data/spec/data/myer-full.config +9 -0
- data/spec/data/myer-multiple.config +14 -0
- data/spec/data/myer.config +6 -0
- data/spec/data/plot-data.csv +31 -0
- data/spec/data/secret-ticket-12345678.json +1 -0
- data/spec/data/secret-ticket-987654321.json +1 -0
- data/spec/plot_spec.rb +9 -0
- data/spec/proc_net_parser_spec.rb +15 -0
- data/spec/shared_examples_for_config.rb +47 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/test_cli_controller_spec.rb +72 -0
- data/spec/ticket_store_spec.rb +86 -0
- data/test/default_test.rb +14 -0
- data/test/test_helper.rb +9 -0
- 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
|
data/lib/myer/config.rb
ADDED
@@ -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
|
data/lib/myer/content.rb
ADDED
@@ -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
|
data/lib/myer/crypto.rb
ADDED
@@ -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
|
data/lib/myer/plot.rb
ADDED