gaddygaddy 0.1.78

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.
@@ -0,0 +1,425 @@
1
+ #
2
+ # Name:
3
+ # gaddygaddy-client.rb
4
+ #
5
+ # Created by: mansson
6
+ #
7
+ # Description:
8
+ #
9
+ #
10
+ #
11
+ # Copyright (c) 2013 GaddyGaddy
12
+ #
13
+ # All rights reserved.
14
+ #
15
+
16
+ APPLICATION_NAME = "gaddygaddy-client"
17
+
18
+ require 'gg_config/gg_config'
19
+ require 'device_info/device_info'
20
+ require 'fileutils'
21
+ require 'utils/hash_monkeypatch'
22
+ require 'json'
23
+ require 'subcommand'
24
+ require 'gaddygaddy-client/chef_management'
25
+ require 'logging/logging'
26
+ require 'restclient'
27
+ require 'systemu'
28
+ require 'utils/request'
29
+ require 'utils/retriable'
30
+ require 'error_constants'
31
+
32
+ REVISION = "$Revision: 1 $"[10..-3].chomp
33
+
34
+ DEFAULT_CHEF_DIR = "/var/chef"
35
+ DEFAULT_CONF_DIR = "/conf"
36
+ DEFAULT_CONF_FILE = "gaddy_*.gcf"
37
+ CONFIG_FILE_NAME = "node.json"
38
+
39
+ class TokenJException < JException;end
40
+ class CreateDeviceJException < JException;end
41
+
42
+ class GaddyGaddy_Client
43
+
44
+ include Logging
45
+ include Subcommands
46
+ include Retriable
47
+ extend Retriable
48
+
49
+ def initialize
50
+ logger.outputters = Log4r::Outputter.stdout
51
+ end
52
+
53
+ def usage
54
+ puts "gaddygaddy-client [OPTIONS]"
55
+ puts "The client to run for the GaddyGaddy service"
56
+ puts "For more information about the commands run gaddygaddy-client --help"
57
+ puts "Revision :#{REVISION}"
58
+ puts
59
+ end
60
+
61
+ def read_options
62
+ # This hash will hold all of the options
63
+ # parsed from the command-line by
64
+ # OptionParser.
65
+ @options = {}
66
+
67
+
68
+ global_options do |opts|
69
+ opts.banner = "Usage: gaddygaddy-client [options] [subcommand [options]]"
70
+ opts.description = "GaddyGaddy client for the GaddyGaddy service"
71
+ opts.separator ""
72
+ opts.separator "Global options are:"
73
+
74
+ @options[:conf_dir] = DEFAULT_CONF_DIR
75
+ opts.on( '--config_dir CONF-DIR', "Directory containing the client configuration files, default is #{DEFAULT_CONF_DIR}" ) do |conf_dir|
76
+ @options[:conf_dir] = conf_dir
77
+ end
78
+
79
+ @options[:conf_file] = DEFAULT_CONF_FILE
80
+ opts.on( '--config_file CONF-FILE', "File name (without) path for the configuration file, default is #{DEFAULT_CONF_FILE}" ) do |conf_file|
81
+ @options[:conf_file] = conf_file
82
+ end
83
+
84
+ @options[:log_file] = nil
85
+ opts.on( '-L', '--logfile FILE', 'Write log to FILE, defaults to STDOUT' ) do|file|
86
+ @options[:log_file] = file
87
+ end
88
+
89
+ @options[:file_host] = "config.gaddygaddy.com:84"
90
+ opts.on( '--file_host FILE_HOST', 'The file host to get files like cookbooks from' ) do|file_host|
91
+ @options[:file_host] = file_host
92
+ end
93
+
94
+ @options[:host] = "ap.gaddygaddy.com:82"
95
+ opts.on( '-H', '--host HOST', 'The host to connect to' ) do|host|
96
+ @options[:host] = host
97
+ end
98
+
99
+ @options[:log_level] = 'info'
100
+ opts.on( '-l', '--log_level level', 'Set the log level (debug, info, warn, error, fatal)' ) do|level|
101
+ @options[:log_level] = level
102
+ end
103
+
104
+ @options[:test_mode] = false
105
+ opts.on( '--test_mode', 'Used for testing, will only retry once for example' ) do
106
+ @options[:test_mode] = true
107
+ end
108
+
109
+ @options[:token] = nil
110
+ opts.on( '-t', '--token TOKEN', 'The token to be used to access' ) do|token|
111
+ @options[:token] = token
112
+ end
113
+
114
+
115
+
116
+ end
117
+
118
+ add_help_option
119
+
120
+ command :get_cookbooks do |opts|
121
+ opts.banner = "Usage: get_cookbooks [options]"
122
+ opts.description = "Will download the cookbooks from the GaddyGaddy service"
123
+ @options[:chef_dir] = DEFAULT_CHEF_DIR
124
+ opts.on( '--chef_dir CHEF-DIR', "The chef dir to place the files into, default is #{DEFAULT_CHEF_DIR}" ) do |chef_dir|
125
+ @options[:chef_dir] = chef_dir
126
+ end
127
+ @options[:cookbooks_version] = nil
128
+ opts.on( '--cookbooks_version COOKBOOKS_VERSION', "Override the setting of which cookbook version to download" ) do |cookbooks_version|
129
+ @options[:cookbooks_version] = cookbooks_version
130
+ end
131
+ end
132
+
133
+ command :chef_config do |opts|
134
+ opts.banner = "Usage: chef_config [options]"
135
+ opts.description = "Will download the config and place it in a directory"
136
+ end
137
+
138
+ command :init_config do |opts|
139
+ opts.banner = "Usage: init_config [options]"
140
+ opts.description = "Will init the config, will check that config files doesn't exist"
141
+ end
142
+
143
+ command :verify_installation do |opts|
144
+ opts.banner = "Usage: verify_installation[options]"
145
+ opts.description = "Will verify the installation and check for gaddy file and network configuration"
146
+ end
147
+ cmd = nil
148
+ begin
149
+ cmd = opt_parse
150
+ mandatory = []
151
+ missing = mandatory.select{ |param| @options[param].nil? }
152
+ unless missing.empty?
153
+ puts "Missing options: #{missing.join(', ')}" #
154
+ puts opt_parse #
155
+ exit 1
156
+ end
157
+ unless cmd
158
+ puts "No command is specified\n\n"
159
+ usage
160
+ puts print_actions
161
+ exit 1
162
+ end
163
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument #
164
+ puts $!.to_s # Friendly output when parsing fails
165
+ puts opt_parse #
166
+ exit 1 #
167
+ end
168
+ cmd
169
+ end
170
+
171
+ def gg_config
172
+ @gg_config ||= GGConfig.new(:conf_dir => @options[:conf_dir], :conf_file => @options[:conf_file])
173
+ end
174
+
175
+ def get_property(property_name)
176
+ file_name = File.join(conf_dir, "gaddygaddy.property")
177
+ value = nil
178
+ if File.exists? file_name
179
+ prop_file = File.open(File.join(conf_dir, "gaddygaddy.property"), "r")
180
+ prop_file.read_lines.each do |line|
181
+ prop, prop_value = line.split("=")
182
+ if prop.strip == property_name
183
+ value = value.strip
184
+ end
185
+ end
186
+ end
187
+ value
188
+ end
189
+
190
+ # Get the config directory
191
+ def conf_dir
192
+ @options[:conf_dir]
193
+ end
194
+
195
+ # Should try to get the host from options or config
196
+
197
+ def get_host
198
+ @options[:host]
199
+ end
200
+
201
+ # Get the version of cookbook installed at the system
202
+
203
+ def get_installed_cookbook_version
204
+ get_property "gaddygaddy.cookbooks.installed_version"
205
+ end
206
+
207
+ def get_cookbook_version
208
+ url = Request.get_base_url(get_host) + "/chef/cookbooks_version/1/#{user_id_salt}/#{device_id}/#{get_token}"
209
+ response = Request.client_service_get url
210
+ raise "Could not get cookbook version" unless response['cookbooks_version']
211
+ logger.debug "Got cookbook version: #{response}"
212
+ response['cookbooks_version']
213
+ end
214
+
215
+ def device_id
216
+ gg_config.config[:device_id]
217
+ end
218
+
219
+ def get_chef_config_file
220
+ url = Request.get_base_url(get_host) + "/chef/node_json/1/#{user_id_salt}/#{device_id}/#{get_token}"
221
+ response = Request.client_service_get url
222
+ raise "Could not get config file for device #{device_id}" unless response["run_list"].length > 0
223
+ logger.debug "Got config file: #{response}"
224
+ response
225
+ end
226
+
227
+ def get_chef_config
228
+ begin
229
+ FileUtils.mkdir_p conf_dir
230
+ rescue Exception => e
231
+ raise e.inspect
232
+ end
233
+ conf_content = get_chef_config_file
234
+ chef_config = conf_content['variables']
235
+ chef_config['run_list'] = conf_content['run_list']
236
+
237
+ conf_file = File.open File.join(conf_dir, CONFIG_FILE_NAME), "w"
238
+ conf_file.write chef_config.to_json
239
+ conf_file.close
240
+ end
241
+
242
+ # Check that the config file exist and has valid content, this will also be validated toward the server
243
+ def config_valid
244
+ config_ok = false
245
+ config_file_name = Dir.glob(File.join(conf_dir, @options[:conf_file]))[0]
246
+ logger.debug "Will validate config for #{config_file_name}"
247
+ if File.exists?(config_file_name)
248
+ begin
249
+ conf_file = File.open(config_file_name)
250
+ config = JSON.parse(conf_file.read)
251
+ rescue Exception => e
252
+ raise "Could not read from file #{config_file_name}"
253
+ end
254
+ logger.debug "Have read from file #{config_file_name}"
255
+ config_ok = config["device_id"]
256
+ end
257
+ # TODO validate the client against the client service
258
+ config_ok
259
+ end
260
+
261
+ # Get a device id, to start with only implemented for Raspberry
262
+ def hardware_id
263
+ hardware_id = `cat /proc/cpuinfo|grep Serial`.strip[10..-1].to_s.strip
264
+ logger.debug "The device id is #{hardware_id}"
265
+ if hardware_id == ""
266
+ hardware_id = 'test_device'
267
+ end
268
+ raise JNoDeviceIDFound.new({:message => "Could not found a device id for this computer, a device id is needed, see further help"}) unless hardware_id
269
+ hardware_id
270
+ end
271
+
272
+ # Get teh user name from the config
273
+ def user_email
274
+ # This to avoid characters like + replaced with space
275
+ gg_config.get_user_email
276
+ end
277
+
278
+ def user_id_salt
279
+ gg_config.get_user_id_salt
280
+ end
281
+
282
+
283
+ # Create a new config by requesting it from the server
284
+ def create_new_config
285
+ url = Request.get_base_url(get_host) + "/chef/add_device_for_user/1/#{CGI.escape(CGI.escape(user_email))}"
286
+ response = Request.client_service_get url
287
+ url_config = URI.encode(Request.get_base_url(get_host) + "/device/config_file/1/#{response['device_id']}/#{response['user_id_salt']}/#{response['token']}")
288
+ response_config = Request.client_service_get url_config
289
+ logger.debug response_config.class
290
+ logger.debug response_config.inspect
291
+ response = response_config
292
+ raise JNoDeviceIDFound if response_config['status'].to_i != 0
293
+ gg_config.config = response['config']
294
+ gg_config.save
295
+ end
296
+
297
+ # init the configuration by trying to get the node config, if that doesn't exist then create a new config
298
+ def init_config
299
+ begin
300
+ get_chef_config_file
301
+ rescue JNoGaddyGaddyConfigFile
302
+ end
303
+ return if config_valid
304
+ create_new_config
305
+ @device_info.post
306
+ end
307
+
308
+ def get_token
309
+ # TODO get this from config file
310
+ token = gg_config.config[:token]
311
+ raise TokenJException.new({:status => ERR_NO_VALID_TOKEN, :message => "No token specified"}) unless token
312
+ token
313
+ end
314
+
315
+ def run_cmd(cmd)
316
+ status, stdout, stderr = systemu cmd
317
+ raise "Could not run command: #{cmd}, stdout is #{stdout}, error message is #{stderr}" if status != 0
318
+ end
319
+
320
+ def get_cookbook
321
+ version = @options[:cookbooks_version] ? @options[:cookbooks_version] : get_cookbook_version.to_s
322
+ installed_version = get_installed_cookbook_version.to_s
323
+ tmp_file = "/tmp/cookbooks-#{version}.tar.gz"
324
+ port = 80
325
+ file_host = @options[:file_host]
326
+ # The file host should be without http or port
327
+ file_host = file_host.split("://")[1] if file_host.index("://")
328
+ file_host,port = file_host.split(":") if file_host.index(":")
329
+ Net::HTTP.start(file_host, port) do |http|
330
+ begin
331
+ file = open(tmp_file, 'wb')
332
+ cookbook_path = '/' + URI.encode("pkg/gg_chef_#{version}.tar.gz")
333
+ logger.debug "Will request the cookbooks files from http://#{file_host}:#{port}#{cookbook_path}"
334
+ result = http.request_get(cookbook_path) do |response|
335
+ response.read_body do |segment|
336
+ file.write(segment)
337
+ end
338
+ end
339
+ logger.debug "The tar cookbook request response was #{result.inspect}"
340
+ ensure
341
+ file.close
342
+ end
343
+ end
344
+ logger.debug "Will untar the file to #{@options[:chef_dir]} and then remove file #{tmp_file}"
345
+ FileUtils.mkdir_p @options[:chef_dir]
346
+ cmd = "tar -C #{@options[:chef_dir]} -zxvf #{tmp_file}"
347
+ run_cmd cmd
348
+ cmd_remove = "rm #{tmp_file}"
349
+ run_cmd cmd_remove
350
+ end
351
+
352
+ def self.alert(type)
353
+ alert_timing = case type
354
+ when :no_config_file
355
+ {:on => 0.1, :off => 0.1,:alert_text => 'Could not found any gaddy config file in /conf/gaddy_XXXX.gcf'}
356
+ when :no_network
357
+ {:on => 1,:off => 1, :alert_text => 'Could not connect to internet, network configuration seams broken'}
358
+ end
359
+ begin
360
+ `echo none >/sys/class/leds/led0/trigger`
361
+ count = 0
362
+ send_info_count = 20 / (alert_timing[:on] + alert_timing[:off])
363
+ while true do
364
+ if (count % send_info_count) == 0
365
+ puts alert_timing[:alert_text]
366
+ `/usr/bin/espeak "#{alert_timing[:alert_text]}"`
367
+ end
368
+ `echo 1 >/sys/class/leds/led0/brightness`
369
+ sleep alert_timing[:on]
370
+ `echo 0 >/sys/class/leds/led0/brightness`
371
+ sleep alert_timing[:off]
372
+ count += 1
373
+ end
374
+ ensure
375
+ `echo mmc0 >/sys/class/leds/led0/trigger`
376
+ end
377
+ end
378
+
379
+ def self.ip_to_verify
380
+ '8.8.8.8'
381
+ end
382
+
383
+ def self.network_ok?
384
+ ping_result = `ping -w 10 #{ip_to_verify} -c 1`
385
+ logger.debug "Result of ping is #{ping_result}"
386
+ ! ping_result.index("100% packet loss")
387
+ end
388
+
389
+ # Verify that we have a conf file and verify network connection
390
+ def self.verify_installation
391
+ conf_file = Dir.glob(File.join('/','conf', "gaddy*.gcf"))
392
+ alert(:no_config_file) if conf_file.empty?
393
+ alert(:no_network) unless network_ok?
394
+ end
395
+
396
+ def run
397
+ cmd = read_options
398
+ begin
399
+ Retriable.set_test_mode if @options[:test_mode]
400
+ @device_info = DeviceInfo.new(get_host, gg_config)
401
+ set_log_level @options[:log_level]
402
+ set_log_file @options[:log_file] if @options[:log_file]
403
+ logger.info "Will start #{APPLICATION_NAME} with command #{cmd}"
404
+ case cmd
405
+ when "get_cookbooks"
406
+ get_cookbook
407
+ when "chef_config"
408
+ get_chef_config
409
+ when "init_config"
410
+ init_config
411
+ when "verify_installation"
412
+ self.class.verify_installation
413
+ else
414
+ usage
415
+ raise "No valid command entered, the command is #{cmd}"
416
+ end
417
+
418
+ rescue Exception => e
419
+ logger.error e.message
420
+ logger.error "Enable full stack trace with -l DEBUG" unless logger.debug?
421
+ logger.error "Backtrace:\n\t#{e.backtrace.join("\n\t")}" if logger.debug?
422
+
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,162 @@
1
+ #
2
+ # Name:
3
+ # config.rb
4
+ #
5
+ # Created by: GaddyGaddy
6
+ #
7
+ # Description:
8
+ #
9
+ #
10
+ #
11
+ # Copyright (c) 2013 GaddyGaddy
12
+ #
13
+ # All rights reserved.
14
+ #
15
+
16
+
17
+ $LOAD_PATH << File.expand_path('../../../../gg_common/lib',__FILE__)
18
+
19
+ require 'error_constants'
20
+ require 'jexception'
21
+ require_relative '../logging/logging'
22
+ require 'topic_constants'
23
+
24
+ class GGConfig
25
+ include Logging
26
+
27
+ def initialize(options = {:conf_dir => "/conf", :conf_file => "gaddy_*.gcf"} )
28
+ @conf_file = options[:conf_file]
29
+ @conf_dir = File.expand_path(options[:conf_dir])
30
+ end
31
+
32
+
33
+ def device_id
34
+ config[:device_id]
35
+ end
36
+
37
+ def get_device_name
38
+ device_name = get_config_for(:device_name,"host","device")
39
+ unless valid_host_name?(device_name)
40
+ raise JException.new({:message => "The host name #{device_name} is not a valid host name",
41
+ :status => ERR_NO_VALID_HOSTNAME,
42
+ :extra_info => {:host_name => device_name}
43
+ })
44
+ end
45
+ device_name
46
+ end
47
+
48
+ def token
49
+ config[:token]
50
+ end
51
+
52
+ def user_email
53
+ get_config_for(:user_email,"user","user")
54
+ end
55
+
56
+ def user_id_salt
57
+ get_config_for(:user_id_salt,"user","user_id_salt")
58
+ end
59
+
60
+
61
+
62
+ # Get the config file name
63
+ def config_file_name
64
+ Dir.glob(File.join(@conf_dir, @conf_file))[0]
65
+ end
66
+
67
+ def read_config_file
68
+ begin
69
+ config_file = File.open(config_file_name)
70
+ file_content = config_file.read
71
+ config = JSON.parse(file_content)
72
+ logger.debug "Have read config file #{config_file_name} and found content #{config.to_s}"
73
+ rescue Exception => e
74
+ raise JNoGaddyGaddyConfigFile.new({:message => e.message})
75
+ end
76
+ config.symbolize_keys!
77
+ end
78
+
79
+ def config
80
+ @config_content ||= read_config_file
81
+ end
82
+
83
+ def config= (new_config)
84
+ @config_content = new_config.symbolize_keys!
85
+ end
86
+
87
+ def valid_host_name?(host_name)
88
+ host_name.size <= 63 and not (host_name.rindex('-', 0) or host_name.index('-', -1) or host_name.scan(/[^a-z\d-]/i).any?)
89
+ end
90
+
91
+ def save
92
+ unless valid_config?
93
+ raise JCouldNotSaveConfigFileException.new({:message => "Missing configuration information, could not save config file for #{config.inspect}"})
94
+ end
95
+ begin
96
+ config_file = File.open(config_file_name,"w")
97
+ config_file.write config.to_json.to_s
98
+ rescue Exception => e
99
+ raise JCouldNotSaveConfigFileException.new({:message => e.message})
100
+ end
101
+ end
102
+
103
+
104
+ # Generic method to get the initial config for a config_type, could be device_name or user_name, will also try
105
+ # to find all kind of configs files with device_name or user_name
106
+ def get_config_for(config_type, conf_name, extra_conf_file)
107
+ config_value = nil
108
+ begin
109
+ if config
110
+ logger.debug "Config exist and is #{config} looking for config type #{config_type}"
111
+ config_value = config[config_type]
112
+ end
113
+ rescue Exception => e
114
+ logger.info "Could not find a gaddygaddy config file, message is #{e.message}"
115
+ end
116
+ unless config_value
117
+ files = Dir.glob(File.join(@conf_dir, 'config.*'))
118
+ files << File.join(@conf_dir, extra_conf_file) if File.exist? File.join(@conf_dir, extra_conf_file)
119
+ files.each do |conf_file_name|
120
+ logger.debug "The file #{conf_file_name} has file size #{File.size(conf_file_name)}"
121
+ if File.size(conf_file_name).to_i < 1000
122
+ conf_file = File.open(conf_file_name,"r")
123
+ lines = conf_file.readlines
124
+ # Check if there are not a lot of lines in the config
125
+ if lines.size < 4
126
+ lines.each do |line|
127
+ if line.index("=")
128
+ args = line.split("=")
129
+ # Validate that the syntax of the property is ok
130
+ if args.size == 2
131
+ config_value = args[1].strip if args[0].strip == conf_name || args[0].strip == "config_value"
132
+ else
133
+ logger.info "The file #{conf_file_name} has too many = in the line #{line}"
134
+ end
135
+ else
136
+ # Validate that there are no spaces in the config_value
137
+ config_value = line.strip
138
+ end
139
+ end
140
+ else
141
+ logger.info "The file #{conf_file_name} is not a config file as it's too many lines"
142
+ end
143
+ else
144
+ logger.info "Will not check file #{conf_file_name} as it is to big"
145
+ end
146
+ end
147
+ end
148
+ # Check that we have found a config_value and that the config_value is a valid host name, if not raise a exception with topic info to be able to help the user further
149
+ unless config_value
150
+ raise JNoGaddyGaddyConfigFile.new({:message => "Could not found any file with config in the config dir #{@conf_dir}",
151
+ :topic => TPC_NO_CONFIG_FILE,
152
+ :extra_info => {:conf_dir => @conf_dir}
153
+ })
154
+ end
155
+ config_value
156
+ end
157
+
158
+ def valid_config?
159
+ config[:user_id_salt] && config[:token] && config[:device_name]
160
+ end
161
+
162
+ end