gitchefsync 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ #helper file for parsing knife commands
2
+ #this may get refactored
3
+
4
+ require 'gitchefsync/git_util'
5
+ require 'gitchefsync/errors'
6
+ require 'gitchefsync/io_util'
7
+ require 'gitchefsync/config'
8
+
9
+ module Gitchefsync
10
+
11
+ class KnifeUtil
12
+
13
+ def initialize(knife, wd)
14
+ @knife = knife
15
+ @wd = wd
16
+ end
17
+
18
+ #needs knife on the command path
19
+ #parses the knife command "cookbook list -a"
20
+ #returns a list of cookbooks
21
+ def listCookbooks
22
+ list = Array.new
23
+ str = FS.cmd "cd #{@wd} && #{@knife} cookbook list -a"
24
+ arr_str = str.split(/\n/)
25
+ arr_str.each do |line|
26
+ cb_name, *cb_versions = line.split(' ')
27
+ cb_versions.each do |cb_version|
28
+ list << Cookbook.new(cb_name, cb_version)
29
+ end
30
+ end
31
+ list
32
+ end
33
+
34
+ #get a list of existing environment names on chef server
35
+ def listEnv
36
+ list = Array.new
37
+ str = FS.cmd "cd #{@wd} && #{@knife} environment list"
38
+ environments = str.split(/\n/)
39
+ environments.each do |env|
40
+ list << env.strip
41
+ end
42
+ list
43
+ end
44
+
45
+ #get a list of existing data bags items (in [bag, item] pairs) on chef server
46
+ def listDB
47
+ list = Array.new
48
+ str = FS.cmd "cd #{@wd} && #{@knife} data bag list"
49
+ data_bags = str.split(/\n/)
50
+ data_bags.each do |bag|
51
+ data_bag_items = showDBItem bag.strip
52
+ data_bag_items.each do |item|
53
+ list << [bag.strip, item.strip]
54
+ end
55
+ end
56
+ list
57
+ end
58
+
59
+ #get a list of existing data bag items (from specified bag) on chef server
60
+ def showDBItem bag
61
+ list = Array.new
62
+ str = FS.cmd "cd #{@wd} && #{@knife} data bag show #{bag}"
63
+ data_bag_items = str.split(/\n/)
64
+ data_bag_items.each do |item|
65
+ list << item.strip
66
+ end
67
+ list
68
+ end
69
+
70
+ #get a list of existing role names on chef server
71
+ def listRole
72
+ list = Array.new
73
+ str = FS.cmd "cd #{@wd} && #{@knife} role list"
74
+ roles = str.split(/\n/)
75
+ roles.each do |role|
76
+ list << role.strip
77
+ end
78
+ list
79
+ end
80
+
81
+ #checks if the cookbook name and version exist in the
82
+ #array of cookbooks
83
+ #@param name - name of cookbook
84
+ #@param version - version of cookbook
85
+ #@param list - the list of cookbooks - from listCookbooks
86
+ def inList( name, version, list)
87
+ found = false
88
+ list.each do |item|
89
+ found = true if ( (name == item.name) and (version == item.version))
90
+ end
91
+ found
92
+ end
93
+
94
+ #Checks to see if cookbook given is in list
95
+ #uses inList method to determine it
96
+ def isCBinList(cookbook, list)
97
+ return inList( cookbook.name, cookbook.version, list)
98
+ end
99
+
100
+ #returns a list of are in list1 that are not in list2
101
+ def subtract(list1,list2)
102
+ list = Array.new
103
+ list1.each do |cookbook|
104
+ if !isCBinList(cookbook,list2)
105
+ list << cookbook
106
+ end
107
+ end
108
+ list
109
+ end
110
+
111
+ #delete a cookbook from the server
112
+ def delCookbook(cb)
113
+ begin
114
+ FS.cmd("cd #{@wd} && knife cookbook delete #{cb.name} #{cb.version} -y" )
115
+ rescue CmdError => e
116
+ Gitchefsync.logger.error "event_id=cb_del:#{e.message}:e=#{e.backtrace}"
117
+ end
118
+ end
119
+
120
+ #Parse metadata.rb from a given directory path
121
+ def parseMetaData(path)
122
+ #Gitchefsync.logger.debug "Parsing metadata: #{path}"
123
+ if !File.exists?(File.join(path, "metadata.rb"))
124
+ raise NoMetaDataError
125
+ end
126
+ contents = ""
127
+ begin
128
+ file = File.new(File.join(path, "metadata.rb"), "r")
129
+
130
+ contents = file.read
131
+ version = attr_val(contents,"version")
132
+ name = attr_val(contents,"name")
133
+
134
+ if name.nil?
135
+ Gitchefsync.logger.warn "event_id=parse_meta_err_name:msg=cannot be resolved, deferring to directory name #{path}"
136
+ name = File.basename path
137
+ end
138
+ #parse maintainer information
139
+ maintainer = attr_val(contents,"maintainer")
140
+ email = attr_val(contents,"maintainer_email")
141
+
142
+ #puts "matched:#{name}:#{version}"
143
+ return Cookbook.new(name, version,maintainer,email)
144
+ rescue Exception => e
145
+ puts e.backtrace
146
+ Gitchefsync.logger.error "#{e.backtrace}"
147
+ raise KnifeError, "Unable to parse data: file=#{path}/metadata.rb #{contents}"
148
+ ensure
149
+ file.close unless file.nil?
150
+ end
151
+ end
152
+
153
+ def attr_val(contents, name)
154
+ m1 = contents.match(/#{name}\s+['|"](.*)['|"]/)
155
+ val = nil
156
+ if m1 != nil && m1.length == 2
157
+ val = m1[1]
158
+ else
159
+ Gitchefsync.logger.warn "event_id=parse_warn:name=#{name}"
160
+ end
161
+ val
162
+ end
163
+ end #end class
164
+
165
+ #A cookbook description
166
+ #may include a hash or other description
167
+ class Cookbook
168
+
169
+ def initialize(name,version,maintainer = "", maintainer_email = "")
170
+ @name = name
171
+ @version = version
172
+ @maintainer = maintainer
173
+ @maintainer_email = maintainer_email
174
+ end
175
+
176
+ def name
177
+ @name
178
+ end
179
+
180
+ def version
181
+ @version
182
+ end
183
+
184
+ def maintainer
185
+ @maintainer
186
+ end
187
+
188
+ def setMaintainer(maintainer)
189
+ @maintainer = maintainer
190
+ end
191
+
192
+ def maintainer_email
193
+ @maintainer_email
194
+ end
195
+
196
+ def setMaintainer_email(maintainer_email)
197
+ @maintainer_email = maintainer_email
198
+ end
199
+
200
+ def to_s
201
+ return @name + "_" + @version
202
+ end
203
+
204
+ #name convention of how berks packages
205
+ def berksTar
206
+ return @name + "-" + @version + ".tar.gz"
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,46 @@
1
+ module Gitchefsync
2
+ module Log
3
+ #Wrapped sys log Logger
4
+ #Overload all the logger methods - although substitution is not covered
5
+ class SysLogger
6
+
7
+ #TODO: may define syslog open at this point
8
+ def initialize(name)
9
+ begin
10
+ Syslog.open(name, Syslog::LOG_PID, Syslog::LOG_LOCAL1)
11
+ rescue Exception => e
12
+ puts "Syslog error: #{e.message}"
13
+ end
14
+
15
+ end
16
+
17
+ def debug(*args)
18
+ log(Syslog::LOG_DEBUG,args[0])
19
+ end
20
+
21
+ def info(*args)
22
+ log(Syslog::LOG_INFO,args[0])
23
+ end
24
+
25
+ def warn(*args)
26
+ log(Syslog::LOG_WARNING,args[0])
27
+ end
28
+
29
+ def error(*args)
30
+ log(Syslog::LOG_ERR,args[0])
31
+ end
32
+
33
+ def fatal(*args)
34
+ log(Syslog::LOG_EMERG,args[0])
35
+ end
36
+
37
+ def log ( level, msg)
38
+ begin
39
+ Syslog.log(level, msg)
40
+ ensure
41
+ #Syslog.close
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,74 @@
1
+ require 'gitchefsync/audit'
2
+ require 'gitchefsync/io_util'
3
+ require 'net/smtp'
4
+
5
+ module Gitchefsync
6
+
7
+ class Notification
8
+
9
+ def initialize(smtp="mail.rim.net", from="mandolin@blackberry.com",to='mandolin@blackberry.com', msg="")
10
+ @to = to
11
+ @from = from
12
+ @smtp = smtp = Net::SMTP.start(smtp, 25)
13
+ @hostname = FS.cmd "hostname"
14
+ end
15
+
16
+ def notifyFromAudit(audit_dir, audit_type)
17
+ audit = Audit.new(audit_dir,audit_type)
18
+ audit_list = audit.latestAuditItems
19
+
20
+ audit_list.each do |audit_item|
21
+ if audit_item.ex != nil
22
+ h = audit_item.to_hash
23
+ msg = "From: gichefsync <mandolin@blackberry.com>\nTo: #{h[:maintainer]} #{h[:maintainer_email]}\nSubject: gitchefsync failure\n"
24
+ msg << "Alert from Hostname: #{@hostname}\n\n"
25
+ msg << "Attention!\n\n"
26
+ msg << "gitchefsync has identified you as the maintainer of this artifact\n"
27
+ msg << "====================================\n"
28
+ msg << "#{h[:name]}:#{h[:version]}\n"
29
+ msg << "====================================\n"
30
+ msg << "#{h[:exception]}"
31
+
32
+ sendTo(h[:maintainer_email],msg)
33
+ Gitchefsync.logger.info("event_id=email_sent=#{h[:maintainer_email]} ")
34
+ end
35
+ end
36
+ Gitchefsync.logger.info("event_id=notification_complete:audit_type=#{audit_type}")
37
+ end
38
+
39
+ #Aggregates a single email to the "to" email parameter
40
+ def singleNotifyFromAudit(audit_dir,audit_type,to)
41
+ audit = Audit.new(audit_dir,audit_type)
42
+ audit_list = audit.latestAuditItems
43
+ msg = "From: gichefsync <mandolin@blackberry.com>\nTo: #{to}\nSubject: gitchefsync failure: summary\n\n"
44
+ msg << "Alert from Hostname: #{@hostname}\n\n"
45
+ audit_list.each do |audit_item|
46
+ h = audit_item.to_hash
47
+
48
+ if h[:exception] != nil
49
+ ver = ""
50
+ if !h[:version].empty? then ver = ":" + h[:version] end
51
+
52
+ msg << "item: #{h[:name]}#{ver} was NOT processed with status #{h[:action]} "
53
+ msg << "\nERROR #{h[:exception]}"
54
+ else
55
+ msg << "item: #{h[:name]} was NOT processed with status #{h[:action]} "
56
+ end
57
+ msg << "\n\n"
58
+ end
59
+ sendTo(to,msg)
60
+ end
61
+
62
+ def send(body)
63
+ @smtp.send_message body, @from, @to
64
+ end
65
+
66
+ def sendTo(send_to, body)
67
+ @smtp.send_message body, @from, send_to
68
+ end
69
+
70
+ def close
71
+ @smtp.finish
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,64 @@
1
+ require 'gitlab'
2
+ require 'gitchefsync/errors'
3
+ module Gitchefsync
4
+
5
+ module Parser
6
+
7
+ def self.parseOpts (args)
8
+ options = {}
9
+ begin
10
+
11
+ opt_parser = OptionParser.new do |opts|
12
+ opts.banner = "Usage: sync_all.rb --private_token=xyz OR LDAP credentials"
13
+ options[:private_token] = ''
14
+ opts.on('-t', '--private_token token','gitlab private token') do |token|
15
+ options[:private_token] = token
16
+ end
17
+ options[:config_file] = './sync-config.json'
18
+ opts.on('-c', '--config file','path to config file') do |token|
19
+ options[:config_file] = token
20
+ end
21
+ options[:login] = ''
22
+ opts.on('-l','--login login','Required when token not set') do |login|
23
+ options[:login] = login
24
+ end
25
+ options[:password] = ''
26
+ opts.on('-p','--password password','Required when token not set') do |pass|
27
+ options[:password] = pass
28
+ end
29
+ opts.on('-s','--syslog',"Enable syslog") do |syslog|
30
+ options[:syslog] = true
31
+ end
32
+ opts.on('-u', '--giturl', "Gitlab url") do |giturl|
33
+ Gitlab.endpoint = giturl
34
+ end
35
+ end
36
+
37
+ opt_parser.parse! (args)
38
+
39
+ json = File.read(options[:config_file])
40
+
41
+ j_config = JSON.parse(json)
42
+ options[:config] = j_config
43
+ options[:git_local] = j_config['working_directory']
44
+ options[:knife_config] = j_config['knife_file']
45
+ options[:groups] = j_config['git_groups']
46
+ options[:stage_cookbook_dir] = j_config['tmp_dir']
47
+
48
+ #set gitlab token and ensure git url for backward compatibility
49
+ if Gitlab.endpoint == nil
50
+ Gitlab.endpoint = 'https://gitlab.rim.net/api/v3'
51
+ end
52
+ if options[:private_token].empty? && !options[:login].empty? && !options[:password].empty?
53
+ puts "using credentials"
54
+ options[:private_token] = Gitlab.session(options[:login],options[:password]).to_hash['private_token']
55
+ end
56
+
57
+ rescue Exception => e
58
+ puts e.backtrace
59
+ raise(ConfigError,e.message,e.backtrace)
60
+ end
61
+ options
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,161 @@
1
+ require 'timeout'
2
+
3
+ module Gitchefsync
4
+ class ScheduleSync
5
+
6
+ def initialize()
7
+ options = Gitchefsync.options
8
+ config = Gitchefsync.configuration
9
+
10
+ @lock_filename = config['lock_filename'] || 'sync.lock'
11
+ @lock_timeout = config['lock_timeout'] || 10
12
+ @lock_timeout = @lock_timeout.to_i
13
+
14
+ @master_sync_timeout = config['master_sync_timeout'] || 600
15
+ @master_sync_timeout = @master_sync_timeout.to_i
16
+
17
+ @sous_rsync_user = config['sous_rsync_user'] || 'chefsync'
18
+ @sous_rsync_host = config['sous_rsync_host'] || ''
19
+ @sous_rsync_src = config['sous_rsync_src'] || config['stage_dir'] || '/opt/gitchefsync/staging/'
20
+ @sous_rsync_dest = config['sous_rsync_dest'] ||
21
+ File.join(File::SEPARATOR, config['stage_dir'].split(File::SEPARATOR)[1..-2]) ||
22
+ '/opt/gitchefsync/'
23
+ @sous_rsync_options = config['sous_rsync_options'] || '-ar --delete'
24
+ @sous_rsync_excludes = config['sous_rsync_excludes'] || '.chef .snapshot'
25
+
26
+ @sous_sync_timeout = config['sous_sync_timeout'] || 600
27
+ @sous_sync_timeout = @sous_sync_timeout.to_i
28
+ end
29
+
30
+ def obtainExclusiveLock
31
+ Gitchefsync.logger.info "event_id=attempt_to_lock_file:lock_filename=#{@lock_filename}"
32
+ lock_file = File.open(@lock_filename, File::RDWR|File::CREAT, 0644)
33
+
34
+ begin
35
+ Timeout::timeout(@lock_timeout) { lock_file.flock(File::LOCK_EX) }
36
+ rescue
37
+ Gitchefsync.logger.fatal "event_id=unable_to_lock_file:lock_filename=#{@lock_filename}"
38
+ exit 1
39
+ end
40
+
41
+ lock_file
42
+ end
43
+
44
+ def master
45
+ lock_file = obtainExclusiveLock
46
+
47
+ begin
48
+ Timeout::timeout(@master_sync_timeout) do
49
+ Gitchefsync.logger.info "event_id=master_sync_starting"
50
+
51
+ #Setup and check Gitlab API endpoint
52
+ Gitlab.endpoint = 'http://gitlab.rim.net/api/v3'
53
+ Gitchefsync.checkGit
54
+
55
+ Gitchefsync.syncEnv
56
+ Gitchefsync.syncCookbooks
57
+ Gitchefsync.reconcile
58
+ cleanTmp()
59
+ Gitchefsync.logger.info "event_id=master_sync_completed"
60
+
61
+ # MAND-615 - This file will signal its ok for this directory to be sous sync target
62
+ File.write(File.join(@sous_rsync_src, "master_sync_completed"), "")
63
+ end
64
+ rescue Timeout::Error
65
+ Gitchefsync.logger.fatal "event_id=master_sync_timed_out:master_sync_timeout=#{@master_sync_timeout}"
66
+ exit 1
67
+ rescue => e
68
+ Gitchefsync.logger.error "event_id=caught_exception:msg=#{e.message}"
69
+ end
70
+ lock_file.close
71
+ end
72
+
73
+ def sous
74
+ lock_file = obtainExclusiveLock
75
+
76
+ begin
77
+ Timeout::timeout(@sous_sync_timeout) do
78
+ Gitchefsync.logger.info "event_id=sous_sync_starting"
79
+
80
+ exclude = ""
81
+ @sous_rsync_excludes.split(" ").each do |pattern|
82
+ exclude = "#{exclude} --exclude #{pattern}"
83
+ end
84
+
85
+ if @sous_rsync_host.empty?
86
+ Gitchefsync.logger.fatal "event_id=sous_rsync_host_not_configured"
87
+ exit 1
88
+ end
89
+
90
+ master_sync_completed = File.join(@sous_rsync_src, "master_sync_completed")
91
+ master_sync_completed_cmd = "ssh #{@sous_rsync_user}@#{@sous_rsync_host} ls #{master_sync_completed} 2>/dev/null"
92
+ Gitchefsync.logger.info "event_id=check_master_sync_completed:cmd=#{master_sync_completed_cmd}"
93
+ master_sync_completed_stdout = FS.cmd "#{master_sync_completed_cmd}"
94
+
95
+ # MAND-615 - Do not perform an rsync on #{master_sync_completed} target if empty.
96
+ # Avoid doing rsync command and staged cookbook/env upload in situation where master
97
+ # chef server was re-instantiated and master sync has yet to run once.
98
+ if master_sync_completed_stdout.empty?
99
+ Gitchefsync.logger.fatal "event_id=missing_master_sync_completed_file:master_sync_completed=#{master_sync_completed}"
100
+ exit 1
101
+ end
102
+
103
+ rsync_cmd = "rsync #{@sous_rsync_options} #{exclude} #{@sous_rsync_user}@#{@sous_rsync_host}:#{@sous_rsync_src} #{@sous_rsync_dest} 2>/dev/null"
104
+ Gitchefsync.logger.info "event_id=execute_rsync:cmd=#{rsync_cmd}"
105
+ FS.cmd "#{rsync_cmd}"
106
+
107
+ Gitchefsync.stagedUpload
108
+ Gitchefsync.syncEnv
109
+ Gitchefsync.reconcile
110
+ cleanTmp()
111
+ Gitchefsync.logger.info "event_id=sous_sync_completed"
112
+ end
113
+ rescue Timeout::Error
114
+ Gitchefsync.logger.fatal "event_id=sous_sync_timed_out:sous_sync_timeout=#{@sous_sync_timeout}"
115
+ exit 1
116
+ rescue => e
117
+ Gitchefsync.logger.error "event_id=caught_exception:msg=#{e.message}"
118
+ end
119
+ lock_file.close
120
+ end
121
+
122
+ #Due to ridley bug (4.0.0+ possibly earlier) -clean the tmp directories
123
+ #takes current "Date" and cleans up
124
+ def cleanTmp
125
+ ts_str = "/tmp/d" + Date.today.strftime("%Y%m%d") + "-*"
126
+ Gitchefsync.logger.info "clean up of #{ts_str}"
127
+ FS.cmdNoError "sudo rm -fr #{ts_str}"
128
+ end
129
+ end
130
+
131
+ def self.runMasterSync
132
+ scheduleSync = ScheduleSync.new()
133
+ scheduleSync.master
134
+ end
135
+
136
+ def self.runSousSync
137
+ #force loocal sync
138
+ options[:config]['sync_local'] = "true"
139
+
140
+ #Make sure sous sync only runs on the primary node
141
+ drbd_connection_state = FS.cmd("sudo drbdadm cstate chef_disk",
142
+ { "TERM" => "xterm", "PATH" => "/usr/sbin:/usr/bin:/bin:/sbin" })
143
+ drbd_role = FS.cmd("sudo drbdadm role chef_disk",
144
+ { "TERM" => "xterm", "PATH" => "/usr/sbin:/usr/bin:/bin:/sbin" })
145
+
146
+ drbd_connection_state.delete!("\n")
147
+ drbd_role.delete!("\n")
148
+
149
+ connected = drbd_connection_state.match(/Connected/)
150
+ role = drbd_role.match(/^Primary/)
151
+
152
+ if connected and role
153
+ Gitchefsync.logger.info "event_id=proceed_to_sous_sync:drbd_connection_state=#{connected}:drbd_role=#{role}"
154
+ scheduleSync = ScheduleSync.new()
155
+ scheduleSync.sous
156
+ elsif
157
+ Gitchefsync.logger.fatal "event_id=abort_sous_sync:drbd_connection_state=#{connected}:drbd_role=#{role}"
158
+ exit 1
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,3 @@
1
+ module Gitchefsync
2
+ VERSION = "0.6.1"
3
+ end