gaddygaddy 0.1.78

Sign up to get free protection for your applications and to get access to all the features.
@@ -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