1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 import base64
17 from datetime import datetime
18 import difflib
19 import os
20 import sys
21
22 from config_common import handler_base, utils, cfg_exceptions
23 from config_common.rhn_log import log_debug, die
24 from config_common.file_utils import f_date, ostr_to_sym
25 from spacewalk.common.usix import next
26 from rhn.i18n import bstr, sstr
27
28
29 -class Handler(handler_base.HandlerBase):
30 _usage_options = "[options] file [ file ... ]"
31 _options_table = [
32 handler_base.HandlerBase._option_class(
33 '-c', '--channel', action="store",
34 help="Get file(s) from this config channel",
35 ),
36 handler_base.HandlerBase._option_class(
37 '-r', '--revision', action="store",
38 help="Use this revision",
39 ),
40 handler_base.HandlerBase._option_class(
41 '-d', '--dest-file', action="store",
42 help="Remote file to compare to",
43 ),
44 handler_base.HandlerBase._option_class(
45 '-t', '--topdir', action="store",
46 help="Directory to which all file paths are relative",
47 ),
48 ]
50 log_debug(2)
51
52 if self.options.dest_file and self.options.topdir:
53 die(6, "Error: conflicting options --dest-file and --topdir")
54
55 if len(self.args) == 0:
56 die(0, "No files supplied (use --help for help)")
57
58 channel = self.options.channel
59
60 if not channel:
61 die(6, "Config channel not specified")
62
63 r = self.repository
64 if not r.config_channel_exists(channel):
65 die(6, "Error: config channel %s does not exist" % channel)
66
67 topdir = self.options.topdir
68 revision = self.options.revision
69
70 files_to_diff = []
71
72 files = [utils.normalize_path(x) for x in self.args]
73 files_count = len(files)
74
75 if files_count != 1 and revision is not None:
76 die(8, "--revision can only be used with a single file")
77
78 if self.options.dest_file:
79 if files_count != 1:
80 die(7, "--dest-file accepts a single file")
81
82 files_to_diff.append((files[0], self.options.dest_file))
83
84 elif topdir:
85 if not os.path.isdir(topdir):
86 die(8, "--topdir specified, but `%s' not a directory" %
87 topdir)
88
89
90 topdir = utils.rm_trailing_slash(topdir)
91
92 for f in files:
93 if not f.startswith(topdir):
94 die(8, "--topdir %s specified, but file `%s' doesn't comply"
95 % (topdir, f))
96 if os.path.isdir(f) and not os.path.islink(f):
97 die(8, "Cannot diff %s; it is a directory" % f)
98 files_to_diff.append((f, f[len(topdir):]))
99 else:
100 for f in files:
101 if os.path.isdir(f) and not os.path.islink(f):
102 die(8, "Cannot diff %s; it is a directory" % f)
103 files_to_diff.append((f, f))
104
105 for (local_file, remote_file) in files_to_diff:
106 sys.stdout.write(
107 self.diff_file(channel, remote_file, local_file, revision))
108
110 """ Returns true if acl, ownership, type or selinux context differ.
111 fsrc is config file retrieved from xmlrpc, fdst is output of make_stat_info()
112 """
113 return (fsrc['filemode'] != fdst['mode']) or \
114 (fsrc['username'] != fdst['user']) or (fsrc['groupname'] != fdst['group']) or \
115 (fsrc['selinux_ctx'] != fdst['selinux_ctx'])
116
117 - def diff_file(self, channel, path, local_file, revision):
118 r = self.repository
119 try:
120 info = r.get_raw_file_info(channel, path, revision)
121 if 'encoding' in info and info['file_contents']:
122 if info['encoding'] == 'base64':
123 info['file_contents'] = base64.decodestring(bstr(info['file_contents']))
124 else:
125 die(9, 'Error: unknown encoding %s' % info['encoding'])
126 except cfg_exceptions.RepositoryFileMissingError:
127 die(2, "Error: no such file %s (revision %s) in config channel %s"
128 % (path, revision, channel))
129 if os.path.islink(local_file) and info['filetype'] != 'symlink' :
130 die(8, "Cannot diff %s; the file on the system is a symbolic link while the file in the channel is not. " % local_file)
131 if info['filetype'] == 'symlink' and not os.path.islink(local_file) :
132 die(8, "Cannot diff %s; the file on the system is not a symbolic link while the file in the channel is. " % local_file)
133 if info['filetype'] == 'symlink':
134 src_link = info['symlink']
135 dest_link = os.readlink(local_file)
136 if src_link != os.readlink(local_file):
137 return "Symbolic links differ. Channel: '%s' -> '%s' System: '%s' -> '%s' \n " % (path,src_link, path, dest_link)
138 return ""
139
140 response_output = ""
141 content_differs = False
142 if 'is_binary' in info and info['is_binary'] == 'Y':
143 from_content = info['file_contents']
144 to_file = open(local_file, 'rb')
145 to_content = to_file.read()
146 to_file.close()
147 if len(from_content) != len(to_content):
148 content_differs = True
149 else:
150 for i in range(len(from_content)):
151 if from_content[i] != to_content[i]:
152 content_differs = True
153 break
154 if content_differs:
155 response_output = "Binary file content differs\n"
156 else:
157 fromlines = sstr(info['file_contents']).splitlines(1)
158 tofile = open(local_file, 'r')
159 tolines = tofile.readlines()
160 tofile.close()
161 diff_output = difflib.unified_diff(fromlines, tolines, info['path'], local_file)
162 first_row = second_row = ''
163 try:
164 first_row = next(diff_output)
165
166
167 content_differs = True
168 second_row = next(diff_output)
169 response_output = ''.join(list(diff_output))
170 except StopIteration:
171 pass
172
173 file_stat = os.lstat(local_file)
174 local_info = r.make_stat_info(local_file, file_stat)
175
176 if not 'selinux_ctx' in local_info:
177 local_info['selinux_ctx'] = ''
178 if 'selinux_ctx' not in info:
179 info['selinux_ctx'] = ''
180 if not content_differs and not self.__attributes_differ(info, local_info):
181 return ""
182 else:
183 template = "%s %s\t%s\tattributes: %s %s %s %s\tconfig channel: %s\trevision: %s"
184 if 'modified' not in info:
185 info['modified'] = ''
186 first_row = template % ('---', path, str(info['modified']), ostr_to_sym(info['filemode'], info['filetype']),
187 info['username'], info['groupname'], info['selinux_ctx'], channel,
188 info['revision'],
189 )
190 second_row = template % ('+++', local_file, f_date(datetime.fromtimestamp(local_info['mtime'])), ostr_to_sym(local_info['mode'], 'file'),
191 local_info['user'], local_info['group'], local_info['selinux_ctx'], 'local file', None
192 )
193 return first_row + '\n' + second_row + '\n' + response_output
194