Package backend :: Package server :: Module configFilesHandler
[hide private]
[frames] | no frames]

Source Code for Module backend.server.configFilesHandler

  1  # 
  2  # Copyright (c) 2008--2016 Red Hat, Inc. 
  3  # 
  4  # This software is licensed to you under the GNU General Public License, 
  5  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  6  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
  8  # along with this software; if not, see 
  9  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 10  # 
 11  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 12  # granted to use or replicate Red Hat trademarks that are incorporated 
 13  # in this software or its documentation. 
 14  # 
 15  # 
 16  # Config file handler (base class) 
 17  # 
 18   
 19  import base64 
 20  import os 
 21  try: 
 22      #  python 2 
 23      import xmlrpclib 
 24  except ImportError: 
 25      #  python3 
 26      import xmlrpc.client as xmlrpclib 
 27  import sys 
 28  import hashlib 
 29   
 30  from spacewalk.common.usix import raise_with_tb 
 31  from spacewalk.common import rhnFlags 
 32  from spacewalk.common.rhnLog import log_debug 
 33  from spacewalk.common.rhnConfig import CFG 
 34  from spacewalk.common.checksum import getStringChecksum 
 35  from spacewalk.common.rhnException import rhnFault, rhnException 
 36   
 37  from spacewalk.server import rhnSQL, rhnUser, rhnCapability 
 38  from spacewalk.server.rhnHandler import rhnHandler 
 39   
 40  from spacewalk.server.config_common.templated_document import ServerTemplatedDocument, var_interp_prep 
 41   
 42   
 43  import rhnSession 
 44   
 45  # Exceptions 
 46   
 47   
48 -class BaseConfigFileError(Exception):
49
50 - def __init__(self, args):
51 Exception.__init__(self, *args)
52 53
54 -class ConfigFileError(BaseConfigFileError):
55
56 - def __init__(self, file, *args):
57 BaseConfigFileError.__init__(self, args) 58 self.file = file
59 60
61 -class ConfigFileExistsError(ConfigFileError):
62 pass
63 64
65 -class ConfigFileVersionMismatchError(ConfigFileError):
66 pass
67 68
69 -class ConfigFileMissingDelimError(ConfigFileError):
70 pass
71 72
73 -class ConfigFileMissingInfoError(ConfigFileError):
74 pass
75 76
77 -class ConfigFileMissingContentError(ConfigFileError):
78 pass
79 80
81 -class ConfigFileTooLargeError(ConfigFileError):
82 pass
83 84
85 -class ConfigFileExceedsQuota(ConfigFileError):
86 pass
87 88
89 -class ConfigFilePathIncomplete(ConfigFileError):
90 pass
91 92 # Base handler class 93 94
95 -class ConfigFilesHandler(rhnHandler):
96
97 - def __init__(self):
98 log_debug(3) 99 rhnHandler.__init__(self) 100 self.functions = { 101 'rhn_login': 'login', 102 'test_session': 'test_session', 103 'max_upload_fsize': 'max_upload_file_size', 104 } 105 self.org_id = None
106 107 # Returns a reference to a callable method
108 - def get_function(self, function):
109 if function not in self.functions: 110 return None 111 112 # Turn compression on by default 113 rhnFlags.set('compress_response', 1) 114 return getattr(self, self.functions[function])
115 116 # returns max filesize that will be uploaded
117 - def max_upload_file_size(self):
118 return self._get_maximum_file_size()
119 120 # Generic login function
121 - def login(self, dict):
122 log_debug(1) 123 username = dict.get('username') 124 password = dict.get('password') 125 self.user = rhnUser.search(username) 126 if not self.user or not (self.user.check_password(password)): 127 raise rhnFault(2) 128 129 # Good to go 130 session = self.user.create_session() 131 return session.get_session()
132
133 - def test_session(self, dict):
134 log_debug(3) 135 136 try: 137 self._validate_session(dict.get('session')) 138 except (rhnSession.InvalidSessionError, rhnSession.ExpiredSessionError): 139 return 0 140 141 return 1
142 143 # Helper functions
144 - def _get_delimiters(self):
145 return { 146 'delim_start': CFG.config_delim_start, 147 'delim_end': CFG.config_delim_end, 148 }
149
150 - def _validate_session(self, session):
151 # session_reload will toss an exception if the session 152 # token is invalid... I guess we're letting it percolate 153 # up... 154 # --bretm 155 self.user = rhnUser.session_reload(session) 156 self.org_id = self.user.contact['org_id'] 157 self._check_user_role()
158
159 - def _check_user_role(self):
160 pass
161
162 - def _is_file(self, file):
163 return str(file['config_file_type_id']) == '1'
164 167 168 _query_current_selinux_lookup = rhnSQL.Statement(""" 169 select ci.selinux_ctx from rhnConfigInfo ci, rhnConfigRevision cr, rhnConfigFile cf 170 where ci.id = cr.config_info_id 171 and cf.id = cr.config_file_id 172 and cf.config_file_name_id = lookup_config_filename(:path) 173 and cr.revision = ( 174 select max(mcr.revision) from rhnConfigRevision mcr, rhnConfigFile mcf 175 where mcf.id = mcr.config_file_id 176 and mcf.config_file_name_id = cf.config_file_name_id 177 ) 178 """) 179
180 - def _push_file(self, config_channel_id, file):
181 if not file: 182 # Nothing to do 183 return {} 184 185 # Check for full path on the file 186 path = file.get('path') 187 if not (path[0] == os.sep): 188 raise ConfigFilePathIncomplete(file) 189 190 if 'config_file_type_id' not in file: 191 log_debug(4, "Client does not support config directories, so set file_type_id to 1") 192 file['config_file_type_id'] = '1' 193 # Check if delimiters are present 194 if self._is_file(file) and \ 195 not (file.get('delim_start') and file.get('delim_end')): 196 # Need delimiters 197 raise ConfigFileMissingDelimError(file) 198 199 if not (file.get('user') and file.get('group') and 200 file.get('mode') is not None) and not self._is_link(file): 201 raise ConfigFileMissingInfoError(file) 202 203 # Oracle doesn't like certain binding variables 204 file['username'] = file.get('user', '') 205 file['groupname'] = file.get('group', '') 206 file['file_mode'] = str(file.get('mode', '')) 207 # if the selinux flag is not sent by the client it is set to the last file 208 # revision (or to None (i.e. NULL) in case of first revision) - see the bug 209 # 644985 - SELinux context cleared from RHEL4 rhncfg-client 210 file['selinux_ctx'] = file.get('selinux_ctx', None) 211 if not file['selinux_ctx']: 212 # RHEL4 or RHEL5+ with disabled selinux - set from the last revision 213 h = rhnSQL.prepare(self._query_current_selinux_lookup) 214 h.execute(**file) 215 row = h.fetchone_dict() 216 if row: 217 file['selinux_ctx'] = row['selinux_ctx'] 218 else: 219 file['selinux_ctx'] = None 220 result = {} 221 222 try: 223 224 if self._is_file(file): 225 self._push_contents(file) 226 elif self._is_link(file): 227 file['symlink'] = file.get('symlink') or '' 228 except ConfigFileTooLargeError: 229 result['file_too_large'] = 1 230 231 t = rhnSQL.Table('rhnConfigFileState', 'label') 232 state_id_alive = t['alive']['id'] 233 234 file['state_id'] = state_id_alive 235 file['config_channel_id'] = config_channel_id 236 237 try: 238 self._push_config_file(file) 239 self._push_revision(file) 240 except rhnSQL.SQLSchemaError: 241 e = sys.exc_info()[1] 242 log_debug(4, "schema error", e) 243 rhnSQL.rollback() # blow away the contents that got inserted 244 if e.errno == 20267: 245 # ORA-20267: (not_enough_quota) - Insufficient available quota 246 # for the specified action 247 raise ConfigFileExceedsQuota(file) 248 raise 249 250 return {}
251 252 # A wrapper around _push_file, that also catches exceptions
253 - def push_file(self, config_channel_id, file):
254 try: 255 result = self._push_file(config_channel_id, file) 256 except ConfigFilePathIncomplete: 257 e = sys.exc_info()[1] 258 raise_with_tb(rhnFault(4015, "Full path of file '%s' must be specified" % e.file.get('path'), 259 explain=0), sys.exc_info()[2]) 260 261 except ConfigFileExistsError: 262 e = sys.exc_info()[1] 263 raise_with_tb(rhnFault(4013, "File %s already uploaded" % e.file.get('path'), 264 explain=0), sys.exc_info()[2]) 265 except ConfigFileVersionMismatchError: 266 e = sys.exc_info()[1] 267 raise_with_tb(rhnFault(4012, "File %s uploaded with a different " 268 "version" % e.file.get('path'), explain=0), sys.exc_info()[2]) 269 except ConfigFileMissingDelimError: 270 e = sys.exc_info()[1] 271 raise_with_tb(rhnFault(4008, "Delimiter not specified for file %s" % 272 e.file.get('path'), explain=0), sys.exc_info()[2]) 273 except ConfigFileMissingContentError: 274 e = sys.exc_info()[1] 275 raise_with_tb(rhnFault(4007, "No content sent for file %s" % 276 e.file.get('path'), explain=0), sys.exc_info()[2]) 277 except ConfigFileExceedsQuota: 278 e = sys.exc_info()[1] 279 raise_with_tb(rhnFault(4014, "File size of %s exceeds free quota space" % 280 e.file.get('path'), explain=0), sys.exc_info()[2]) 281 except ConfigFileTooLargeError: 282 e = sys.exc_info()[1] 283 raise_with_tb(rhnFault(4003, "File size of %s larger than %s bytes" % 284 (e.file.get('path'), self._get_maximum_file_size()), 285 explain=0), sys.exc_info()[2]) 286 287 rhnSQL.commit() 288 return result
289 290 _query_content_lookup = rhnSQL.Statement(""" 291 select cc.id, cv.checksum_type, cv.checksum, file_size, contents, is_binary, delim_start, delim_end 292 from rhnConfigContent cc, rhnChecksumView cv 293 where cv.checksum = :checksum 294 and cv.checksum_type = :checksum_type 295 and file_size = :file_size 296 and checksum_id = cv.id 297 """) 298 299 _query_insert_content = rhnSQL.Statement(""" 300 insert into rhnConfigContent 301 (id, checksum_id, file_size, contents, is_binary, delim_start, delim_end) 302 values (:config_content_id, lookup_checksum(:checksum_type, :checksum), 303 :file_size, :contents, :is_binary, :delim_start, :delim_end) 304 """) 305
306 - def _push_contents(self, file):
307 308 checksum_type = 'sha256' # FIXME: this should be configuration option 309 310 file['file_size'] = 0 311 file['is_binary'] = 'N' 312 313 file_path = file.get('path') 314 file_contents = file.get('file_contents') or '' 315 316 if 'enc64' in file and file_contents: 317 file_contents = base64.decodestring(file_contents) 318 319 if 'config_file_type_id' not in file: 320 log_debug(4, "Client does not support config directories, so set file_type_id to 1") 321 file['config_file_type_id'] = '1' 322 323 file['checksum_type'] = checksum_type 324 file['checksum'] = getStringChecksum(checksum_type, file_contents or '') 325 326 if file_contents: 327 file['file_size'] = len(file_contents) 328 329 if file['file_size'] > self._get_maximum_file_size(): 330 raise ConfigFileTooLargeError(file_path, file['file_size']) 331 332 # Is the content binary data? 333 # XXX We may need a heuristic; this is what the web site does, and we 334 # have to be consistent 335 # XXX Yes this is iterating over a string 336 try: 337 file_contents.decode('UTF-8') 338 except UnicodeDecodeError: 339 file['is_binary'] = 'Y' 340 341 h = rhnSQL.prepare(self._query_content_lookup) 342 h.execute(**file) 343 row = h.fetchone_dict() 344 345 if row: 346 db_contents = rhnSQL.read_lob(row['contents']) or '' 347 if file_contents == db_contents: 348 # Same content 349 file['config_content_id'] = row['id'] 350 log_debug(5, "same content") 351 return 352 353 # We have to insert a new file now 354 content_seq = rhnSQL.Sequence('rhn_confcontent_id_seq') 355 config_content_id = content_seq.next() 356 file['config_content_id'] = config_content_id 357 file['contents'] = file_contents 358 359 h = rhnSQL.prepare(self._query_insert_content, 360 blob_map={'contents': 'contents'}) 361 h.execute(**file)
362 363 _query_lookup_symlink_config_info = rhnSQL.Statement(""" 364 select lookup_config_info(null, null, null, :selinux_ctx, lookup_config_filename(:symlink)) id 365 from dual 366 """) 367 368 _query_lookup_non_symlink_config_info = rhnSQL.Statement(""" 369 select lookup_config_info(:username, :groupname, :file_mode, :selinux_ctx, null) id 370 from dual 371 """) 372 373 _query_lookup_config_file = rhnSQL.Statement(""" 374 select id 375 from rhnConfigFile 376 where config_channel_id = :config_channel_id 377 and config_file_name_id = lookup_config_filename(:path) 378 """) 379
380 - def _push_config_file(self, file):
381 config_info_query = self._query_lookup_non_symlink_config_info 382 if self._is_link(file) and file.get("symlink"): 383 config_info_query = self._query_lookup_symlink_config_info 384 385 # Look up the config info first 386 h = rhnSQL.prepare(config_info_query) 387 h.execute(**file) 388 row = h.fetchone_dict() 389 if not row: 390 # Hmm 391 raise rhnException("This query should always return a row") 392 config_info_id = row['id'] 393 file['config_info_id'] = config_info_id 394 395 # Look up the config file itself 396 h = rhnSQL.prepare(self._query_lookup_config_file) 397 h.execute(**file) 398 row = h.fetchone_dict() 399 if row: 400 # Yay we already have this file 401 # Later down the road, we're going to update modified for this 402 # table 403 file['config_file_id'] = row['id'] 404 return 405 406 # Have to insert this config file, gotta use the api to keep quotas up2date... 407 insert_call = rhnSQL.Function("rhn_config.insert_file", 408 rhnSQL.types.NUMBER()) 409 file['config_file_id'] = insert_call(file['config_channel_id'], file['path'])
410 411 _query_lookup_revision = rhnSQL.Statement(""" 412 select id, revision, config_content_id, config_info_id, 413 config_file_type_id 414 from rhnConfigRevision 415 where config_file_id = :config_file_id 416 order by revision desc 417 """) 418
419 - def _push_revision(self, file):
420 # Assume we don't have any revision for now 421 file['revision'] = 1 422 h = rhnSQL.prepare(self._query_lookup_revision) 423 h.execute(**file) 424 row = h.fetchone_dict() 425 if row: 426 # Is it the same revision as this one? 427 428 fields = ['config_content_id', 'config_info_id', 'config_file_type_id'] 429 430 if 'config_file_type_id' not in file: 431 log_debug(4, "Client does not support config directories, so set file_type_id to 1") 432 file['config_file_type_id'] = '1' 433 434 for f in fields: 435 if file.get(f) != row.get(f): 436 break 437 else: # for 438 # All fields are equal 439 file['config_revision_id'] = row['id'] 440 self._update_revision(file) 441 self._update_config_file(file) 442 return 443 444 # A revision already exists, but it's different. Just update the 445 # revision number 446 447 revision = row['revision'] + 1 448 file['revision'] = revision 449 450 # If we got here, we need a new revision 451 self._insert_revision(file) 452 453 if self.user and hasattr(self.user, 'getid'): 454 self._add_author(file, self.user) 455 self._update_config_file(file)
456 457 _query_update_revision = rhnSQL.Statement(""" 458 update rhnConfigRevision 459 set modified = current_timestamp 460 where id = :config_revision_id 461 """) 462
463 - def _update_revision(self, file):
464 h = rhnSQL.prepare(self._query_update_revision) 465 h.execute(**file)
466
467 - def _insert_revision(self, file):
468 insert_call = rhnSQL.Function("rhn_config.insert_revision", 469 rhnSQL.types.NUMBER()) 470 file['config_revision_id'] = insert_call(file['revision'], 471 file['config_file_id'], 472 file.get('config_content_id', None), 473 file['config_info_id'], 474 file['config_file_type_id'])
475 476 _query_update_revision_add_author = rhnSQL.Statement(""" 477 update rhnConfigRevision 478 set changed_by_id = :user_id 479 where id = :rev_id 480 """) 481
482 - def _add_author(self, file, author):
483 h = rhnSQL.prepare(self._query_update_revision_add_author) 484 h.execute(user_id=author.getid(), rev_id=file['config_revision_id'])
485 486 _query_update_config_file = rhnSQL.Statement(""" 487 update rhnConfigFile 488 set latest_config_revision_id = :config_revision_id, 489 state_id = :state_id 490 where config_channel_id = :config_channel_id 491 and config_file_name_id = lookup_config_filename(:path) 492 """) 493
494 - def _update_config_file(self, file):
495 h = rhnSQL.prepare(self._query_update_config_file) 496 h.execute(**file)
497
498 - def _format_file_results(self, row):
499 server = None 500 if self.server: 501 server = var_interp_prep(self.server) 502 503 return format_file_results(row, server=server)
504
505 - def _get_maximum_file_size(self):
506 return CFG.maximum_config_file_size
507
508 - def new_config_channel_id(self):
509 return rhnSQL.Sequence('rhn_confchan_id_seq').next()
510 511
512 -def format_file_results(row, server=None):
513 encoding = '' 514 contents = None 515 contents = rhnSQL.read_lob(row['file_contents']) or '' 516 checksum = row['checksum'] or '' 517 518 if server and (row['is_binary'] == 'N') and contents: 519 520 interpolator = ServerTemplatedDocument(server, 521 start_delim=row['delim_start'], 522 end_delim=row['delim_end']) 523 contents = interpolator.interpolate(contents) 524 if row['checksum_type']: 525 checksummer = hashlib.new(row['checksum_type']) 526 checksummer.update(contents) 527 checksum = checksummer.hexdigest() 528 529 if contents: 530 client_caps = rhnCapability.get_client_capabilities() 531 if client_caps and 'configfiles.base64_enc' in client_caps: 532 encoding = 'base64' 533 contents = base64.encodestring(contents) 534 if row.get('modified', False): 535 m_date = xmlrpclib.DateTime(str(row['modified'])) 536 else: 537 m_date = '' 538 539 return { 540 'path': row['path'], 541 'config_channel': row['config_channel'], 542 'file_contents': contents, 543 'symlink': row['symlink'] or '', 544 'checksum_type': row['checksum_type'] or '', 545 'checksum': checksum, 546 'verify_contents': True, 547 'delim_start': row['delim_start'] or '', 548 'delim_end': row['delim_end'] or '', 549 'revision': row['revision'] or '', 550 'username': row['username'] or '', 551 'groupname': row['groupname'] or '', 552 'filemode': row['filemode'] or '', 553 'encoding': encoding or '', 554 'filetype': row['label'], 555 'selinux_ctx': row['selinux_ctx'] or '', 556 'modified': m_date, 557 'is_binary': row['is_binary'] or '', 558 }
559