rbcli 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +30 -1
- data/README.md +74 -12
- data/examples/mytool +9 -6
- data/lib/rbcli/stateful_systems/state_storage.rb +89 -0
- data/lib/rbcli/stateful_systems/storagetypes/localstate.rb +65 -0
- data/lib/rbcli/stateful_systems/storagetypes/remote_state_connectors/dynamodb.rb +240 -0
- data/lib/rbcli/stateful_systems/storagetypes/remotestate_dynamodb.rb +109 -0
- data/lib/rbcli/util/config.rb +3 -2
- data/lib/rbcli/version.rb +1 -1
- data/lib/rbcli.rb +3 -1
- data/rbcli.gemspec +4 -0
- metadata +48 -3
- data/lib/rbcli/stateful_systems/localstorage/localstate.rb +0 -124
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4368206fc709a9d855156e9a447c8de3715f4916063af8d189a23dba1fb92c1a
|
4
|
+
data.tar.gz: 6f4dbed307cb87216c28a78a9c07774dde6245c3a8615afb921ab0d3d4df9764
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59a004efa92a3a62d0b6a9bfb1e2fc4c0f0ada6c7c61f26d920a85c7903ba1a0282bd2fcdf91e56db8b56c20e9c0e00fbc82fcde6fcae31c631b4e866813a433
|
7
|
+
data.tar.gz: a96255d756e764c65ea9c25616967194cc77922b813dfe8c7bf7601be107e50a705e12ed3641e0945261c12b510b0d5d821d094d0ca8182054c50003fc3a1985
|
data/Gemfile.lock
CHANGED
@@ -1,17 +1,46 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rbcli (0.1.
|
4
|
+
rbcli (0.1.4)
|
5
|
+
aws-sdk-dynamodb (~> 1.6)
|
5
6
|
colorize (~> 0.8)
|
6
7
|
deep_merge (~> 1.2)
|
8
|
+
macaddr (~> 1.7)
|
9
|
+
rufus-scheduler (~> 3.5)
|
7
10
|
|
8
11
|
GEM
|
9
12
|
remote: https://rubygems.org/
|
10
13
|
specs:
|
14
|
+
aws-eventstream (1.0.0)
|
15
|
+
aws-partitions (1.87.0)
|
16
|
+
aws-sdk-core (3.21.2)
|
17
|
+
aws-eventstream (~> 1.0)
|
18
|
+
aws-partitions (~> 1.0)
|
19
|
+
aws-sigv4 (~> 1.0)
|
20
|
+
jmespath (~> 1.0)
|
21
|
+
aws-sdk-dynamodb (1.6.0)
|
22
|
+
aws-sdk-core (~> 3)
|
23
|
+
aws-sigv4 (~> 1.0)
|
24
|
+
aws-sigv4 (1.0.2)
|
11
25
|
colorize (0.8.1)
|
12
26
|
deep_merge (1.2.1)
|
27
|
+
et-orbi (1.1.2)
|
28
|
+
tzinfo
|
29
|
+
fugit (1.1.1)
|
30
|
+
et-orbi (~> 1.1, >= 1.1.1)
|
31
|
+
raabro (~> 1.1)
|
32
|
+
jmespath (1.4.0)
|
33
|
+
macaddr (1.7.1)
|
34
|
+
systemu (~> 2.6.2)
|
13
35
|
minitest (5.11.3)
|
36
|
+
raabro (1.1.5)
|
14
37
|
rake (10.5.0)
|
38
|
+
rufus-scheduler (3.5.0)
|
39
|
+
fugit (~> 1.1, >= 1.1.1)
|
40
|
+
systemu (2.6.5)
|
41
|
+
thread_safe (0.3.6)
|
42
|
+
tzinfo (1.2.5)
|
43
|
+
thread_safe (~> 0.1)
|
15
44
|
|
16
45
|
PLATFORMS
|
17
46
|
ruby
|
data/README.md
CHANGED
@@ -16,6 +16,10 @@ Some of its key features include:
|
|
16
16
|
|
17
17
|
* __Local State Storage__: Easily manage a set of data that persists between runs. You get access to a hash that is automatically kept in-sync with a file on disk.
|
18
18
|
|
19
|
+
* __Remote State__: It works just like Local State Storage, but store the data on a remote server! It can be used in tandem with Local State Storage or on its own. Currently supports AWS DyanmoDB.
|
20
|
+
|
21
|
+
* __State Locking and Sharing__: Share remote state safely between users with built-in locking! When enabled, it makes sure that only one user is accessing the data at any given time.
|
22
|
+
|
19
23
|
## Installation
|
20
24
|
|
21
25
|
RBCli is available on rubygems.org. You can add it to your application's `Gemrile` or `gemspec`, or install it manually via `gem install rbcli`.
|
@@ -62,7 +66,7 @@ Creating a new skeleton command is as easy as running `rbcli init <filename>`. I
|
|
62
66
|
```ruby
|
63
67
|
require 'rbcli'
|
64
68
|
|
65
|
-
Rbcli::
|
69
|
+
Rbcli::Configurate.me do
|
66
70
|
scriptname __FILE__.split('/')[-1] # (Required) This line identifies the tool's executable. You can change it if needed, but this should work for most cases.
|
67
71
|
version '0.1.0' # (Required) The version number
|
68
72
|
description 'This is my test CLI tool.' # (Requierd) A description that will appear when the user looks at the help with -h. This can be as long as needed.
|
@@ -115,7 +119,8 @@ Once parsed, options will be placed in a hash where they can be accessed via the
|
|
115
119
|
|
116
120
|
```ruby
|
117
121
|
Rbcli::Configurate.storage do
|
118
|
-
local_state '/var/mytool/localstate', force_creation: true,
|
122
|
+
local_state '/var/mytool/localstate', force_creation: true, halt_on_error: true # (Optional) Creates a hash that is automatically saved to a file locally for state persistance. It is accessible to all commands at Rbcli.local_state[:yourkeyhere]
|
123
|
+
remote_state_dynamodb table_name: 'mytable', region: 'us-east-1', force_creation: true, halt_on_error: true, locking: true # (Optional) Creates a hash that is automatically saved to a DynamoDB table. It is recommended to keep halt_on_error=true when using a shared state.
|
119
124
|
end
|
120
125
|
```
|
121
126
|
|
@@ -136,10 +141,12 @@ class Test < Rbcli::Command
|
|
136
141
|
|
137
142
|
action do |params, args, global_opts, config| # (Required) Block to execute if the command is called.
|
138
143
|
Rbcli::log.info { 'These logs can go to STDERR, STDOUT, or a file' } # Example log. Interface is identical to Ruby's logger
|
139
|
-
puts "\nArgs:\n#{args}"
|
140
|
-
puts "Params:\n#{params}"
|
141
|
-
puts "Global opts:\n#{global_opts}"
|
142
|
-
puts "Config:\n#{config}"
|
144
|
+
puts "\nArgs:\n#{args}" # Arguments that came after the command on the CLI
|
145
|
+
puts "Params:\n#{params}" # Parameters, as described through the option statements above
|
146
|
+
puts "Global opts:\n#{global_opts}" # Global Parameters, as descirbed in the Configurate section
|
147
|
+
puts "Config:\n#{config}" # Config file values
|
148
|
+
puts "LocalState:\n#{Rbcli.local_state}" # Local persistent state storage (when available) -- if unsure use Rbcli.local_state.nil?
|
149
|
+
puts "RemoteState:\n#{Rbcli.remote_state}" # Remote persistent state storage (when available) -- if unsure use Rbcli.remote_state.nil?
|
143
150
|
puts "\nDone!!!"
|
144
151
|
end
|
145
152
|
end
|
@@ -225,18 +232,19 @@ logger:
|
|
225
232
|
|
226
233
|
```ruby
|
227
234
|
Rbcli::Configurate.storage do
|
228
|
-
local_state '/var/mytool/localstate', force_creation: true,
|
235
|
+
local_state '/var/mytool/localstate', force_creation: true, halt_on_error: true # (Optional) Creates a hash that is automatically saved to a file locally for state persistance. It is accessible to all commands at Rbcli.local_state[:yourkeyhere]
|
236
|
+
remote_state_dynamodb table_name: 'mytable', region: 'us-east-1', force_creation: true, halt_on_error: true, locking: true # (Optional) Creates a hash that is automatically saved to a DynamoDB table. It is recommended to keep halt_on_error=true when using a shared state.
|
229
237
|
end
|
230
238
|
```
|
231
239
|
|
232
|
-
### Local State
|
240
|
+
### <a name="local_state"></a> Local State
|
233
241
|
|
234
242
|
RBCli's local state storage gives you access to a hash that is automatically persisted to disk when changes are made.
|
235
243
|
|
236
244
|
Once configured you can access it with a standard hash syntax:
|
237
245
|
|
238
246
|
```ruby
|
239
|
-
Rbcli.
|
247
|
+
Rbcli.local_state[:yourkeyhere]
|
240
248
|
```
|
241
249
|
|
242
250
|
For performance reasons, the only methods available for use are `=` (assignment operator), `delete`, `each`, and `key?`. Also, the `clear` method has been added, which resets the data back to an empty hash. Keys are accessed via either symbols or strings indifferently.
|
@@ -247,7 +255,7 @@ Every assignment will result in a write to disk, so if an operation will require
|
|
247
255
|
|
248
256
|
```ruby
|
249
257
|
Rbcli::Configurate.storage do
|
250
|
-
local_state '/var/mytool/localstate', force_creation: true,
|
258
|
+
local_state '/var/mytool/localstate', force_creation: true, halt_on_error: true # (Optional) Creates a hash that is automatically saved to a file locally for state persistance. It is accessible to all commands at Rbcli.local_state[:yourkeyhere]
|
251
259
|
end
|
252
260
|
```
|
253
261
|
|
@@ -255,13 +263,67 @@ There are three parameters to configure it with:
|
|
255
263
|
* The `path` as a string (self-explanatory)
|
256
264
|
* `force_creation`
|
257
265
|
* This will attempt to create the path and file if it does not exist (equivalent to an `mkdir -p` and `touch` in linux)
|
258
|
-
* `
|
266
|
+
* `halt_on_error`
|
259
267
|
* RBCli's default behavior is to raise an exception if the file can not be created, read, or updated at any point in time
|
260
|
-
* If this is set to `
|
268
|
+
* If this is set to `false`, RBCli will silence any errors pertaining to file access and will fall back to whatever data is available. Note that if this is enabled, changes made to the state may not be persisted to disk.
|
261
269
|
* If creation fails and file does not exist, you start with an empty hash
|
262
270
|
* If file exists but can't be read, you will have an empty hash
|
263
271
|
* If file can be read but not written, the hash will be populated with the data. Writes will be stored in memory while the application is running, but will not be persisted to disk.
|
264
272
|
|
273
|
+
### <a name="remote_state">Remote State
|
274
|
+
|
275
|
+
RBCli's remote state storage gives you access to a hash that is automatically persisted to a remote storage location when changes are made. It has locking built-in, meaning that multiple users may share remote state without any data consistency issues!
|
276
|
+
|
277
|
+
Once configured you can access it with a standard hash syntax:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
Rbcli.remote_state[:yourkeyhere]
|
281
|
+
```
|
282
|
+
|
283
|
+
This works the same way that [Local State](#local_state) does, with the same performance caveats (try not to do many writes!).
|
284
|
+
|
285
|
+
#### DynamoDB Configuration
|
286
|
+
|
287
|
+
Before DynamoDB can be used, AWS API credentials have to be created and made available. RBCli will attempt to find credentials from the following locations in order:
|
288
|
+
|
289
|
+
1. User's config file
|
290
|
+
2. Environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
|
291
|
+
3. User's AWSCLI configuration at `~/.aws/credentials`
|
292
|
+
|
293
|
+
For more information about generating and storing AWS credentials, see [Configuring the AWS SDK for Ruby](https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html).
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
Rbcli::Configurate.storage do
|
297
|
+
remote_state_dynamodb table_name: 'mytable', region: 'us-east-1', force_creation: true, halt_on_error: true, locking: true # (Optional) Creates a hash that is automatically saved to a DynamoDB table. It is recommended to keep halt_on_error=true when using a shared state.
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
301
|
+
These are the parameters:
|
302
|
+
* `table_name`
|
303
|
+
* The name of the DynamoDB table to use.
|
304
|
+
* `region`
|
305
|
+
* The AWS region that the database is located
|
306
|
+
* `force_creation`
|
307
|
+
* Creates the DynamoDB table if it does not already exist
|
308
|
+
* `halt_on_error`
|
309
|
+
* Similar to the way [Local State](#local_state) works, setting this to `false` will silence any errors in connecting to the DynamoDB table. Instead, your application will simply have access to an empty hash that does not get persisted anywhere.
|
310
|
+
* This is good for use cases that involve using this storage as a cache to "pick up where you left off in case of failure", where a connection error might mean the feature doesn't work but its not important enough to interrupt the user.
|
311
|
+
* `locking`
|
312
|
+
* This enables locking, for when you are sharing state between different instances of the application. For more information see the [section below](#distributed_locking).
|
313
|
+
|
314
|
+
#### <a name="distributed_locking">Distributed Locking and State Sharing
|
315
|
+
|
316
|
+
Distributed Locking allows a remote state to be shared among multiple users of the application without risk of data corruption. To use it, simply set the `locking:` parameter to `true` when enabling remote state (see above).
|
317
|
+
|
318
|
+
This is how locking works:
|
319
|
+
|
320
|
+
1. The application attempts to acquire a lock on the remote state when it starts
|
321
|
+
2. If the backend is locked by a different application, wait and try again
|
322
|
+
3. If it succeeds, the lock is held and refreshed periodically
|
323
|
+
4. When the application exits, the lock is released
|
324
|
+
5. If the application does not refresh its lock, or fails to release it when it exits, the lock will automatically expire within 60 seconds
|
325
|
+
6. If another application steals the lock (unlikely but possible), and the application tries to save data, a `StandardError` will be thrown
|
326
|
+
7. You can manually attempt to lock/unlock by calling `Rbcli.remote_state.lock` or `Rbcli.remote_state.unlock`, respectively.
|
265
327
|
|
266
328
|
## Development
|
267
329
|
|
data/examples/mytool
CHANGED
@@ -35,7 +35,7 @@ Rbcli::Configurate.me do
|
|
35
35
|
puts 'This is a post-command hook. It executes after the command.'
|
36
36
|
end
|
37
37
|
|
38
|
-
first_run halt_after_running:
|
38
|
+
first_run halt_after_running: false do # (Optional) Allows providing a block of code that executes the first time that the application is run on a given system. If `halt_after_running` is set to `true` then parsing will not continue after this code is executed. All subsequent runs will not execute this code.
|
39
39
|
puts "This is the first time the mytool command is run! Don't forget to generate a config file with the `-g` option before continuing."
|
40
40
|
end
|
41
41
|
end
|
@@ -47,7 +47,8 @@ end
|
|
47
47
|
# are configured here.
|
48
48
|
###############################
|
49
49
|
Rbcli::Configurate.storage do
|
50
|
-
local_state '/var/mytool/localstate', force_creation: true,
|
50
|
+
local_state '/var/mytool/localstate', force_creation: true, halt_on_error: true # (Optional) Creates a hash that is automatically saved to a file locally for state persistance. It is accessible to all commands at Rbcli.localstate[:yourkeyhere]
|
51
|
+
remote_state_dynamodb table_name: 'mytable', region: 'us-east-1', force_creation: true, halt_on_error: true, locking: :auto # (Optional) Creates a hash that is automatically saved to a DynamoDB table. It is recommended to keep halt_on_error=true when using a shared state. Locking can be one of (:manual :auto :none) -- see the README for details
|
51
52
|
end
|
52
53
|
|
53
54
|
#########################
|
@@ -67,10 +68,12 @@ class Test < Rbcli::Command
|
|
67
68
|
|
68
69
|
action do |params, args, global_opts, config| # (Required) Block to execute if the command is called.
|
69
70
|
Rbcli::log.info { 'These logs can go to STDERR, STDOUT, or a file' } # Example log. Interface is identical to Ruby's logger
|
70
|
-
puts "\nArgs:\n#{args}"
|
71
|
-
puts "Params:\n#{params}"
|
72
|
-
puts "Global opts:\n#{global_opts}"
|
73
|
-
puts "Config:\n#{config}"
|
71
|
+
puts "\nArgs:\n#{args}" # Arguments that came after the command on the CLI
|
72
|
+
puts "Params:\n#{params}" # Parameters, as described through the option statements above
|
73
|
+
puts "Global opts:\n#{global_opts}" # Global Parameters, as descirbed in the Configurate section
|
74
|
+
puts "Config:\n#{config}" # Config file values
|
75
|
+
puts "LocalState:\n#{Rbcli.local_state}" # Local persistent state storage (when available) -- if unsure use Rbcli.local_state.nil?
|
76
|
+
puts "RemoteState:\n#{Rbcli.remote_state}" # Remote persistent state storage (when available) -- if unsure use Rbcli.remote_state.nil?
|
74
77
|
puts "\nDone!!!"
|
75
78
|
end
|
76
79
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
|
5
|
+
module Rbcli::State
|
6
|
+
|
7
|
+
## Main State Class
|
8
|
+
class StateStorage
|
9
|
+
|
10
|
+
def initialize path, force_creation: false, halt_on_error: true
|
11
|
+
@path = path
|
12
|
+
@force_creation = force_creation
|
13
|
+
@halt_on_error = halt_on_error
|
14
|
+
|
15
|
+
state_subsystem_init
|
16
|
+
|
17
|
+
base_data = {
|
18
|
+
data: {},
|
19
|
+
rbcli: {}
|
20
|
+
}
|
21
|
+
|
22
|
+
if state_exists?
|
23
|
+
load_state
|
24
|
+
elsif force_creation
|
25
|
+
create_state
|
26
|
+
@data = base_data
|
27
|
+
save_state
|
28
|
+
else
|
29
|
+
raise StandardError "State location #{@path} does not exist or can not be accessed." if @halt_on_error
|
30
|
+
@data = base_data
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def []= key, value
|
35
|
+
@data[:data][key.to_sym] = value
|
36
|
+
save_state
|
37
|
+
@data[:data][key.to_sym]
|
38
|
+
end
|
39
|
+
|
40
|
+
def [] key
|
41
|
+
@data[:data][key.to_sym]
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete key, &block
|
45
|
+
result = @data[:data].delete key.to_sym, block
|
46
|
+
save_state
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def refresh
|
51
|
+
load_state
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear
|
55
|
+
@data[:data] = {}
|
56
|
+
save_state
|
57
|
+
end
|
58
|
+
|
59
|
+
def each &block
|
60
|
+
@data[:data].each &block
|
61
|
+
save_state
|
62
|
+
end
|
63
|
+
|
64
|
+
def key? key
|
65
|
+
@data[:data].key? key.to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_h
|
69
|
+
@data[:data]
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
to_h.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
# For framework's internal use
|
77
|
+
|
78
|
+
def rbclidata key = nil
|
79
|
+
return @data[:rbcli][key.to_sym] unless key.nil?
|
80
|
+
@data[:rbcli]
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_rbclidata key, value
|
84
|
+
@data[:rbcli][key.to_sym] = value
|
85
|
+
save_state
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
## Configuration Interface
|
5
|
+
module Rbcli::ConfigurateStorage
|
6
|
+
@data[:localstate] = nil
|
7
|
+
|
8
|
+
def self.local_state path, force_creation: false, halt_on_error: false
|
9
|
+
@data[:localstate] = Rbcli::State::LocalStorage.new(path, force_creation: force_creation, halt_on_error: halt_on_error)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
## User Interface
|
14
|
+
module Rbcli
|
15
|
+
def self.local_state
|
16
|
+
Rbcli::ConfigurateStorage.data[:localstate]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
## Local State Module
|
21
|
+
module Rbcli::State
|
22
|
+
|
23
|
+
class LocalStorage < StateStorage
|
24
|
+
|
25
|
+
def state_subsystem_init
|
26
|
+
@path = File.expand_path @path
|
27
|
+
end
|
28
|
+
|
29
|
+
def state_exists?
|
30
|
+
File.exists? @path
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_state
|
34
|
+
begin
|
35
|
+
FileUtils.mkdir_p File.dirname(@path)
|
36
|
+
FileUtils.touch @path
|
37
|
+
rescue Errno::EACCES => e
|
38
|
+
error "Can not create file #{@path}. Please make sure the directory is writeable." if @halt_on_error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def load_state
|
43
|
+
begin
|
44
|
+
@data = JSON.parse(File.read(@path)).deep_symbolize!
|
45
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
46
|
+
error "Can not read from file #{@path}. Please make sure the file exists and is readable." if @halt_on_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def save_state
|
51
|
+
begin
|
52
|
+
File.write @path, JSON.dump(@data)
|
53
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
54
|
+
error "Can not write to file #{@path}. Please make sure the file exists and is writeable." if @halt_on_error
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def error text
|
59
|
+
raise LocalStateError.new "Error accessing local state: #{text}"
|
60
|
+
end
|
61
|
+
|
62
|
+
class LocalStateError < StandardError; end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'aws-sdk-dynamodb'
|
2
|
+
require 'macaddr'
|
3
|
+
require 'digest/sha2'
|
4
|
+
require 'rufus-scheduler'
|
5
|
+
|
6
|
+
module Rbcli::State::RemoteConnectors
|
7
|
+
class DynamoDB
|
8
|
+
|
9
|
+
def self.save_defaults aws_access_key_id: nil, aws_secret_access_key: nil
|
10
|
+
Rbcli::Config::add_categorized_defaults :dynamodb_remote_state, 'Remote State Settings - requires DynamoDB', {
|
11
|
+
access_key_id: {
|
12
|
+
description: 'AWS Access Key ID -- leave as null to look for AWS credentials on system. See: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html',
|
13
|
+
value: aws_access_key_id
|
14
|
+
},
|
15
|
+
secret_access_key: {
|
16
|
+
description: 'AWS Secret Access Key -- leave as null to look for AWS credentials on system.',
|
17
|
+
value: aws_secret_access_key
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize dynamodb_table, region, aws_access_key_id, aws_secret_access_key, locking: false, lock_timeout: 60
|
23
|
+
@region = region
|
24
|
+
@dynamo_table_name = dynamodb_table
|
25
|
+
@item_name = Rbcli::configuration[:scriptname]
|
26
|
+
@locking = locking
|
27
|
+
@scheduler = nil
|
28
|
+
@lock_timeout = lock_timeout
|
29
|
+
|
30
|
+
@dynamo_client = Aws::DynamoDB::Client.new(
|
31
|
+
region: @region,
|
32
|
+
access_key_id: aws_access_key_id,
|
33
|
+
secret_access_key: aws_secret_access_key
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_table
|
38
|
+
# We only need to create the table
|
39
|
+
unless table_exists?
|
40
|
+
print "Creating DynmoDB Table. Please wait..."
|
41
|
+
@dynamo_client.create_table(
|
42
|
+
{
|
43
|
+
attribute_definitions: [
|
44
|
+
{
|
45
|
+
attribute_name: "Script Name",
|
46
|
+
attribute_type: "S"
|
47
|
+
}
|
48
|
+
],
|
49
|
+
key_schema: [
|
50
|
+
{
|
51
|
+
attribute_name: "Script Name",
|
52
|
+
key_type: "HASH"
|
53
|
+
}
|
54
|
+
],
|
55
|
+
provisioned_throughput: {
|
56
|
+
read_capacity_units: 5,
|
57
|
+
write_capacity_units: 5,
|
58
|
+
},
|
59
|
+
table_name: @dynamo_table_name,
|
60
|
+
}
|
61
|
+
)
|
62
|
+
wait_for_table_creation
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def table_exists?
|
67
|
+
@dynamo_client.list_tables.table_names.to_a.include? @dynamo_table_name
|
68
|
+
end
|
69
|
+
|
70
|
+
def object_exists?
|
71
|
+
begin
|
72
|
+
item = @dynamo_client.get_item(
|
73
|
+
{
|
74
|
+
key: {'Script Name' => @item_name},
|
75
|
+
table_name: @dynamo_table_name,
|
76
|
+
}
|
77
|
+
)
|
78
|
+
return (!item.item.nil?)
|
79
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_object
|
85
|
+
lock_or_wait
|
86
|
+
item = @dynamo_client.get_item(
|
87
|
+
{
|
88
|
+
key: {'Script Name' => @item_name},
|
89
|
+
table_name: @dynamo_table_name,
|
90
|
+
}
|
91
|
+
).item
|
92
|
+
item.delete 'Script Name'
|
93
|
+
item
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_object datahash
|
97
|
+
raise StandardError "DynamoDB has been locked by another user since the last change. Please try again later." if locked?
|
98
|
+
lock_or_wait
|
99
|
+
@dynamo_client.put_item(
|
100
|
+
{
|
101
|
+
table_name: @dynamo_table_name,
|
102
|
+
item: datahash.merge({'Script Name' => @item_name})
|
103
|
+
}
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def lock
|
108
|
+
@dynamo_client.put_item(
|
109
|
+
{
|
110
|
+
table_name: @dynamo_table_name,
|
111
|
+
item: {
|
112
|
+
'Script Name' => "#{@item_name}_lock",
|
113
|
+
'locked' => true,
|
114
|
+
'locked_until' => (Time.now + @lock_timeout).getutc.strftime('%s'),
|
115
|
+
'locked_by' => Digest::SHA2.hexdigest(Mac.addr)
|
116
|
+
}
|
117
|
+
}
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def unlock
|
122
|
+
@dynamo_client.put_item(
|
123
|
+
{
|
124
|
+
table_name: @dynamo_table_name,
|
125
|
+
item: {
|
126
|
+
'Script Name' => "#{@item_name}_lock",
|
127
|
+
'locked' => false,
|
128
|
+
'locked_until' => Time.now.getutc.strftime('%s'),
|
129
|
+
'locked_by' => false
|
130
|
+
}
|
131
|
+
}
|
132
|
+
)
|
133
|
+
@scheduler.shutdown :kill if @scheduler
|
134
|
+
@scheduler = nil
|
135
|
+
end
|
136
|
+
|
137
|
+
def locked?
|
138
|
+
lockdata = get_lockdata
|
139
|
+
(lockdata['locked']) and (lockdata['locked_until'].to_i > Time.now.getutc.to_i) and (lockdata['locked_by'] != Digest::SHA2.hexdigest(Mac.addr))
|
140
|
+
end
|
141
|
+
|
142
|
+
def lock_or_wait recursed = false
|
143
|
+
return true unless @locking
|
144
|
+
delay_in_seconds = 2
|
145
|
+
lockdata = get_lockdata
|
146
|
+
|
147
|
+
should_claim = false
|
148
|
+
|
149
|
+
# First, we identify if the lock is active
|
150
|
+
if lockdata['locked']
|
151
|
+
# If the lock is not ours, we have to check the expiration
|
152
|
+
if lockdata['locked_by'] != Digest::SHA2.hexdigest(Mac.addr)
|
153
|
+
# If the lock is not ours, and it has expired, we claim it
|
154
|
+
if lockdata['locked_until'].to_i < Time.now.getutc.to_i
|
155
|
+
should_claim = true
|
156
|
+
# If the lock data is not ours and has not expired, we wait and try again
|
157
|
+
else
|
158
|
+
print 'Acquiring lock on DynamoDB. Please wait..' unless recursed
|
159
|
+
print '.'
|
160
|
+
sleep delay_in_seconds
|
161
|
+
lock_or_wait true
|
162
|
+
end
|
163
|
+
|
164
|
+
# If the lock is ours, we check the expiry
|
165
|
+
else
|
166
|
+
# If the lock is ours and is close to expiry or has expired, we refresh it
|
167
|
+
if lockdata['locked_until'].to_i < (Time.now - (@lock_timeout / 10)).getutc.to_i
|
168
|
+
should_claim = true
|
169
|
+
# If the lock is ours and is not near expiry, do nothing
|
170
|
+
else
|
171
|
+
# Do nothing! But do finish the string that's shown to the user
|
172
|
+
puts 'done!' if recursed
|
173
|
+
end
|
174
|
+
end
|
175
|
+
else # If clearly unlocked, we claim it
|
176
|
+
should_claim = true
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
if should_claim
|
181
|
+
# We attempt to get a lock then validate our success
|
182
|
+
lock
|
183
|
+
# If we succeeded then we set up a scheduler to ensure we keep it
|
184
|
+
lockdata = get_lockdata
|
185
|
+
if (lockdata['locked_by'] == Digest::SHA2.hexdigest(Mac.addr)) and (lockdata['locked_until'].to_i > Time.now.getutc.to_i)
|
186
|
+
# Of course, if the scheduler already exists, we don't bother
|
187
|
+
unless @scheduler
|
188
|
+
@scheduler ||= Rufus::Scheduler.new
|
189
|
+
@scheduler.every "#{@lock_timeout - 2}s" do
|
190
|
+
lock
|
191
|
+
end
|
192
|
+
# We also make sure we release the lock at exit. In case this doesn't happen, the lock will expire on its own
|
193
|
+
at_exit do
|
194
|
+
unlock
|
195
|
+
end
|
196
|
+
end
|
197
|
+
puts 'done!' if recursed
|
198
|
+
# If we failed locking then we need to try the process all over again
|
199
|
+
else
|
200
|
+
print 'Error: Failed to lock DynamoDB. Retrying...'
|
201
|
+
sleep delay_in_seconds
|
202
|
+
lock_or_wait true
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
end # END lock_or_wait
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def wait_for_table_creation
|
212
|
+
delay_in_seconds = 2
|
213
|
+
active = false
|
214
|
+
while not active
|
215
|
+
sleep delay_in_seconds
|
216
|
+
print '.'
|
217
|
+
#@dynamo_table = @dynamo_db.table @dynamo_table_name
|
218
|
+
begin
|
219
|
+
result = @dynamo_client.describe_table({table_name: @dynamo_table_name})
|
220
|
+
active = (result.table.table_status == "ACTIVE")
|
221
|
+
#active = (@dynamo_table.table_status == "ACTIVE")
|
222
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
223
|
+
# We want to ignore this exception since we expect the table to be created at some point.
|
224
|
+
# In real usage this error likely won't occur, and instead we will see table_status == "CREATING"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
puts "done!"
|
228
|
+
end
|
229
|
+
|
230
|
+
def get_lockdata
|
231
|
+
@dynamo_client.get_item(
|
232
|
+
{
|
233
|
+
key: {'Script Name' => "#{@item_name}_lock"},
|
234
|
+
table_name: @dynamo_table_name,
|
235
|
+
}
|
236
|
+
).item
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
## Configuration Interface
|
2
|
+
module Rbcli::ConfigurateStorage
|
3
|
+
@data[:remotestate] = nil
|
4
|
+
@data[:remotestate_init_params] = nil
|
5
|
+
|
6
|
+
def self.remote_state_dynamodb table_name: nil, region: nil, force_creation: false, halt_on_error: true, locking: false
|
7
|
+
raise StandardError "Must decalre `table_name` and `region` to use remote_state_dynamodb" if table_name.nil? or region.nil?
|
8
|
+
@data[:remotestate_init_params] = {
|
9
|
+
dynamodb_table: table_name,
|
10
|
+
region: region,
|
11
|
+
locking: locking
|
12
|
+
}
|
13
|
+
@data[:remotestate] = Rbcli::State::DynamoDBStorage.new(table_name, force_creation: force_creation, halt_on_error: halt_on_error)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
## User Interface
|
18
|
+
module Rbcli
|
19
|
+
def self.remote_state
|
20
|
+
Rbcli::ConfigurateStorage.data[:remotestate]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
## Remote State Module
|
25
|
+
module Rbcli::State
|
26
|
+
|
27
|
+
class DynamoDBStorage < StateStorage
|
28
|
+
|
29
|
+
# def initialize dynamodb_table, region, force_creation: false, halt_on_error: true
|
30
|
+
#
|
31
|
+
# # Set defaults in Rbcli's config
|
32
|
+
# Rbcli::State::RemoteStorage::Connectors::DynamoDB.save_defaults
|
33
|
+
#
|
34
|
+
# # Create DynamoDB Connector
|
35
|
+
# @dynamodb = Rbcli::State::RemoteStorage::Connectors::DynamoDB.new dynamodb_table, region, Rbcli::config[:aws_access_key_id], Rbcli::config[:aws_secret_access_key]
|
36
|
+
#
|
37
|
+
# super dynamodb_table, force_creation: force_creation, halt_on_error: halt_on_error
|
38
|
+
# end
|
39
|
+
|
40
|
+
def state_subsystem_init
|
41
|
+
@locking = Rbcli::ConfigurateStorage::data[:remotestate_init_params][:locking]
|
42
|
+
dynamodb_table = Rbcli::ConfigurateStorage::data[:remotestate_init_params][:dynamodb_table]
|
43
|
+
region = Rbcli::ConfigurateStorage::data[:remotestate_init_params][:region]
|
44
|
+
|
45
|
+
# Set defaults in Rbcli's config
|
46
|
+
Rbcli::State::RemoteConnectors::DynamoDB.save_defaults
|
47
|
+
|
48
|
+
# Create DynamoDB Connector
|
49
|
+
@dynamodb = Rbcli::State::RemoteConnectors::DynamoDB.new dynamodb_table, region, Rbcli::config[:aws_access_key_id], Rbcli::config[:aws_secret_access_key], locking: Rbcli::ConfigurateStorage::data[:remotestate_init_params][:locking]
|
50
|
+
end
|
51
|
+
|
52
|
+
def state_exists?
|
53
|
+
return false unless make_dynamo_call { @dynamodb.table_exists? }
|
54
|
+
make_dynamo_call { @dynamodb.object_exists? }
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_state
|
58
|
+
make_dynamo_call do
|
59
|
+
@dynamodb.create_table
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_state
|
64
|
+
make_dynamo_call do
|
65
|
+
@data = @dynamodb.get_object.deep_symbolize!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def save_state
|
70
|
+
make_dynamo_call do
|
71
|
+
@dynamodb.save_object @data
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def lock
|
76
|
+
@dynamodb.lock_or_wait if @locking
|
77
|
+
end
|
78
|
+
|
79
|
+
def unlock
|
80
|
+
@dynamodb.unlock if @locking
|
81
|
+
end
|
82
|
+
|
83
|
+
def error text
|
84
|
+
raise RemoteStateError.new "Error accessing remote state: #{text}"
|
85
|
+
end
|
86
|
+
|
87
|
+
class RemoteStateError < StandardError;
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def make_dynamo_call &block
|
93
|
+
begin
|
94
|
+
yield
|
95
|
+
rescue Aws::Errors::MissingCredentialsError
|
96
|
+
error "Missing AWS Credentials: unable to sign in. Please put the credentials in your config file or update them on the local system." if @halt_on_error
|
97
|
+
rescue Aws::DynamoDB::Errors::UnrecognizedClientException
|
98
|
+
error "Unauthorized AWS Credentials: unable to sign in. Please check the credentials that you are using to make sure they are valid." if @halt_on_error
|
99
|
+
rescue
|
100
|
+
raise if @halt_on_error
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
require 'rbcli/stateful_systems/storagetypes/remote_state_connectors/dynamodb'
|
data/lib/rbcli/util/config.rb
CHANGED
@@ -114,7 +114,8 @@ module Rbcli::Config
|
|
114
114
|
end
|
115
115
|
|
116
116
|
def self.generate_userconf filename
|
117
|
-
filepath = "#{(filename) ? filename : "#{Dir.pwd}/config.yml"}"
|
117
|
+
filepath = File.expand_path "#{(filename) ? filename : "#{Dir.pwd}/config.yml"}"
|
118
|
+
FileUtils.touch filepath
|
118
119
|
File.write filepath, @config_text
|
119
120
|
File.open(filepath, 'a') do |f|
|
120
121
|
f.puts "# Individual Settings"
|
@@ -128,7 +129,7 @@ module Rbcli::Config
|
|
128
129
|
text += "\n# #{opts[:description]}\n"
|
129
130
|
text += "#{name.to_s}:\n"
|
130
131
|
opts[:config].each do |opt, v|
|
131
|
-
text += " #{opt.to_s}: #{v[:value]}".ljust(30) + " # #{v[:description]}\n"
|
132
|
+
text += " #{opt.to_s}: #{(v[:value].nil?) ? '~' : v[:value]}".ljust(30) + " # #{v[:description]}\n"
|
132
133
|
end
|
133
134
|
end
|
134
135
|
File.open(filepath, 'a') do |f|
|
data/lib/rbcli/version.rb
CHANGED
data/lib/rbcli.rb
CHANGED
@@ -8,7 +8,9 @@ require "rbcli/util"
|
|
8
8
|
|
9
9
|
# STATE TOOLS
|
10
10
|
require "rbcli/stateful_systems/configuratestorage"
|
11
|
-
require "rbcli/stateful_systems/
|
11
|
+
require "rbcli/stateful_systems/state_storage"
|
12
|
+
require "rbcli/stateful_systems/storagetypes/localstate"
|
13
|
+
require "rbcli/stateful_systems/storagetypes/remotestate_dynamodb"
|
12
14
|
# END STATE TOOLS
|
13
15
|
|
14
16
|
require "rbcli/configurate"
|
data/rbcli.gemspec
CHANGED
@@ -40,5 +40,9 @@ Gem::Specification.new do |spec|
|
|
40
40
|
|
41
41
|
spec.add_dependency 'colorize', '~> 0.8'
|
42
42
|
spec.add_dependency 'deep_merge', '~> 1.2'
|
43
|
+
spec.add_dependency 'aws-sdk-dynamodb', '~> 1.6'
|
44
|
+
spec.add_dependency 'macaddr', '~> 1.7'
|
45
|
+
spec.add_dependency 'rufus-scheduler', '~> 3.5'
|
46
|
+
|
43
47
|
#spec.add_dependency 'trollop', '~> 2.1'
|
44
48
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rbcli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Khoury
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,48 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: aws-sdk-dynamodb
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.6'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.6'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: macaddr
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.7'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.7'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rufus-scheduler
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.5'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.5'
|
83
125
|
description: RBCli is a framework to quickly develop command-line tools.
|
84
126
|
email:
|
85
127
|
- akhoury@live.com
|
@@ -106,7 +148,10 @@ files:
|
|
106
148
|
- lib/rbcli/engine/command.rb
|
107
149
|
- lib/rbcli/engine/parser.rb
|
108
150
|
- lib/rbcli/stateful_systems/configuratestorage.rb
|
109
|
-
- lib/rbcli/stateful_systems/
|
151
|
+
- lib/rbcli/stateful_systems/state_storage.rb
|
152
|
+
- lib/rbcli/stateful_systems/storagetypes/localstate.rb
|
153
|
+
- lib/rbcli/stateful_systems/storagetypes/remote_state_connectors/dynamodb.rb
|
154
|
+
- lib/rbcli/stateful_systems/storagetypes/remotestate_dynamodb.rb
|
110
155
|
- lib/rbcli/util.rb
|
111
156
|
- lib/rbcli/util/config.rb
|
112
157
|
- lib/rbcli/util/hash_deep_symbolize.rb
|
@@ -1,124 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
## Configuration Interface
|
5
|
-
module Rbcli::ConfigurateStorage
|
6
|
-
@data[:localstate] = nil
|
7
|
-
|
8
|
-
def self.local_state path, force_creation: false, ignore_file_errors: false
|
9
|
-
@data[:localstate] = Rbcli::LocalState.new path, force_creation: force_creation, ignore_file_errors: ignore_file_errors
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
## User Interface
|
14
|
-
module Rbcli
|
15
|
-
def self.local_state
|
16
|
-
Rbcli::ConfigurateStorage.data[:localstate]
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
|
21
|
-
## Local State Class
|
22
|
-
class Rbcli::LocalState
|
23
|
-
|
24
|
-
def initialize path, force_creation: false, ignore_file_errors: false
|
25
|
-
@path = File.expand_path path
|
26
|
-
@ignore_file_errors = ignore_file_errors
|
27
|
-
|
28
|
-
base_data = {
|
29
|
-
data: {},
|
30
|
-
rbcli: {}
|
31
|
-
}
|
32
|
-
|
33
|
-
if File.exist? @path
|
34
|
-
load
|
35
|
-
elsif force_creation
|
36
|
-
create_file
|
37
|
-
@data = base_data
|
38
|
-
save
|
39
|
-
else
|
40
|
-
error "File #{@path} does not exist." unless @ignore_file_errors
|
41
|
-
@data = base_data
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def []= key, value
|
46
|
-
@data[:data][key.to_sym] = value
|
47
|
-
save
|
48
|
-
@data[:data][key.to_sym]
|
49
|
-
end
|
50
|
-
|
51
|
-
def [] key
|
52
|
-
@data[:data][key.to_sym]
|
53
|
-
end
|
54
|
-
|
55
|
-
def delete key, &block
|
56
|
-
result = @data[:data].delete key.to_sym, block
|
57
|
-
save
|
58
|
-
result
|
59
|
-
end
|
60
|
-
|
61
|
-
def clear
|
62
|
-
@data[:data] = {}
|
63
|
-
save
|
64
|
-
end
|
65
|
-
|
66
|
-
def each &block
|
67
|
-
@data[:data].each &block
|
68
|
-
save
|
69
|
-
end
|
70
|
-
|
71
|
-
def key? key
|
72
|
-
@data[:data].key? key.to_sym
|
73
|
-
end
|
74
|
-
|
75
|
-
def to_h
|
76
|
-
@data[:data]
|
77
|
-
end
|
78
|
-
|
79
|
-
# For internal use
|
80
|
-
|
81
|
-
def rbclidata key = nil
|
82
|
-
return @data[:rbcli][key.to_sym] unless key.nil?
|
83
|
-
@data[:rbcli]
|
84
|
-
end
|
85
|
-
|
86
|
-
def set_rbclidata key, value
|
87
|
-
@data[:rbcli][key.to_sym] = value
|
88
|
-
save
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def create_file
|
94
|
-
begin
|
95
|
-
FileUtils.mkdir_p File.dirname(@path)
|
96
|
-
FileUtils.touch @path
|
97
|
-
rescue Errno::EACCES => e
|
98
|
-
error "Can not create file #{@path}. Please make sure the directory is writeable." unless @ignore_file_errors
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def load
|
103
|
-
begin
|
104
|
-
@data = JSON.parse(File.read(@path)).deep_symbolize!
|
105
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
106
|
-
error "Can not read from file #{@path}. Please make sure the file exists and is readable." unless @ignore_file_errors
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def save
|
111
|
-
begin
|
112
|
-
File.write @path, JSON.dump(@data)
|
113
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
114
|
-
error "Can not write to file #{@path}. Please make sure the file exists and is writeable." unless @ignore_file_errors
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def error text
|
119
|
-
raise Rbcli::LocalStateError.new "Error accessing local state: #{text}"
|
120
|
-
end
|
121
|
-
|
122
|
-
end
|
123
|
-
|
124
|
-
class Rbcli::LocalStateError < StandardError; end
|