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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '091cd6eaa815d90823dbb657bc8307322bc0b272a83cf372eaa68456c65aa504'
4
- data.tar.gz: ac8bc103360d5748382020bf796ee11adb1a14c6bb4147158ad15030582b4371
3
+ metadata.gz: 4368206fc709a9d855156e9a447c8de3715f4916063af8d189a23dba1fb92c1a
4
+ data.tar.gz: 6f4dbed307cb87216c28a78a9c07774dde6245c3a8615afb921ab0d3d4df9764
5
5
  SHA512:
6
- metadata.gz: e2229c9330274945480d37d3547faa7c22b5079ebbc89f880f028859b6d906726ce69b1330302fd8e38b88532c73c0d2fffb1c2be97e14f562c2c847bb46cd94
7
- data.tar.gz: c3239439910f6f2ea594c426f94889eb18be945ea2ae3a335915200e20e217b61b5c396a3639af42b6737810e75c969c45ee31f1563653dff681e0b6426078b3
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.3)
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::configurate do
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, ignore_file_errors: false # (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]
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}" # Arguments that came after the command on the CLI
140
- puts "Params:\n#{params}" # Parameters, as described through the option statements above
141
- puts "Global opts:\n#{global_opts}" # Global Parameters, as descirbed in the Configurate section
142
- puts "Config:\n#{config}" # Config file values
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, ignore_file_errors: false # (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]
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.localstate[:yourkeyhere]
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, ignore_file_errors: false # (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]
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
- * `ignore_file_errors`
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 `true`, 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.
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: true 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.
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, ignore_file_errors: false # (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]
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}" # Arguments that came after the command on the CLI
71
- puts "Params:\n#{params}" # Parameters, as described through the option statements above
72
- puts "Global opts:\n#{global_opts}" # Global Parameters, as descirbed in the Configurate section
73
- puts "Config:\n#{config}" # Config file values
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'
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Rbcli
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
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/localstorage/localstate"
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.3
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-27 00:00:00.000000000 Z
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/localstorage/localstate.rb
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