Hacked By AnonymousFox
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import
#
# Copyright (c) 2018 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
"""
This module contains utilities for manipulating files pertaining to system syspurpose
"""
import collections
import logging
import json
import os
import errno
import io
from syspurpose.utils import system_exit, create_dir, create_file, make_utf8, write_to_file_utf8
from syspurpose.i18n import ugettext as _
# Constants for locations of the two system syspurpose files
USER_SYSPURPOSE_DIR = "/etc/rhsm/syspurpose"
USER_SYSPURPOSE = os.path.join(USER_SYSPURPOSE_DIR, "syspurpose.json")
VALID_FIELDS = os.path.join(USER_SYSPURPOSE_DIR, "valid_fields.json") # Will be used for future validation
CACHE_DIR = "/var/lib/rhsm/cache"
CACHED_SYSPURPOSE = os.path.join(CACHE_DIR, "syspurpose.json") # Stores cached values
# All names that represent syspurpose values locally
ROLE = 'role'
ADDONS = 'addons'
SERVICE_LEVEL = 'service_level_agreement'
USAGE = 'usage'
# Remote values keyed on the local ones
LOCAL_TO_REMOTE = {
ROLE: 'role',
ADDONS: 'addOns',
SERVICE_LEVEL: 'serviceLevel',
USAGE: 'usage'
}
# All known syspurpose attributes
ATTRIBUTES = [ROLE, ADDONS, SERVICE_LEVEL, USAGE]
# Values used in determining changes between client and server
UNSUPPORTED = "unsupported"
log = logging.getLogger(__name__)
def post_process_received_data(data):
"""
Try to solve conflicts in keys
- Server returns key "roles", but it should be "role"
- Server returns key "support_level", but service_level_agreement is used in syspurpose.json
:return: modified dictionary
"""
if 'systemPurposeAttributes' in data:
# Fix
if 'roles' in data['systemPurposeAttributes']:
data['systemPurposeAttributes']['role'] = data['systemPurposeAttributes']['roles']
del data['systemPurposeAttributes']['roles']
if 'support_level' in data['systemPurposeAttributes']:
data['systemPurposeAttributes']['service_level_agreement'] = data['systemPurposeAttributes']['support_level']
del data['systemPurposeAttributes']['support_level']
return data
class SyspurposeStore(object):
"""
Represents and maintains a json syspurpose file
"""
def __init__(self, path, raise_on_error=False):
self.path = path
self.contents = {}
self.raise_on_error = raise_on_error
def read_file(self):
"""
Opens & reads the contents of the store's file based on the 'path' provided to the constructor,
and stores them on this object. If the user doesn't have access rights to the file, the program exits.
:return: False if the contents of the file were empty, or the file doesn't exist; otherwise, nothing.
"""
try:
with io.open(self.path, 'r', encoding='utf-8') as f:
self.contents = json.load(f)
return True
except ValueError:
# Malformed JSON or empty file. Let's not error out on an empty file
if os.path.getsize(self.path):
system_exit(
os.EX_CONFIG,
_("Error: Malformed data in file {}; please review and correct.").format(self.path)
)
return False
except OSError as e:
if e.errno == errno.EACCES and not self.raise_on_error:
system_exit(os.EX_NOPERM, _('Cannot read syspurpose file {}\nAre you root?').format(self.path))
if e.errno == errno.ENOENT and not self.raise_on_error:
log.error('Unable to read file {file}: {error}'.format(file=self.path, error=e))
return False
if self.raise_on_error:
raise e
def create(self):
"""
Create the files necessary for this store
:return: True if changes were made, false otherwise
"""
return create_dir(os.path.dirname(self.path)) or \
self.read_file() or \
create_file(self.path, self.contents)
def add(self, key, value):
"""
Add a value to a list of values specified by key. If the current value specified by the key is scalar/non-list,
it is not overridden, but maintained in the list, along with the new value.
:param key: The name of the list
:param value: The value to append to the list
:return: None
"""
value = make_utf8(value)
key = make_utf8(key)
try:
current_value = self.contents[key]
if current_value is not None and not isinstance(current_value, list):
self.contents[key] = [current_value]
if self.contents[key] is None:
self.contents[key] = []
if value not in self.contents[key]:
self.contents[key].append(value)
else:
return False
except (AttributeError, KeyError):
self.contents[key] = [value]
return True
def remove(self, key, value):
"""
Remove a value from a list specified by key.
If the current value specified by the key is not a list, unset the value.
:param key: The name of the list parameter to manipulate
:param value: The value to attempt to remove
:return: True if the value was in the list, False if it was not
"""
value = make_utf8(value)
key = make_utf8(key)
try:
current_value = self.contents[key]
if current_value is not None and not isinstance(current_value, list) and current_value == value:
return self.unset(key)
if value in current_value:
self.contents[key].remove(value)
else:
return False
return True
except (AttributeError, KeyError, ValueError):
return False
def unset(self, key):
"""
Unsets a key
:param key: The key to unset
:return: boolean
"""
key = make_utf8(key)
# Special handling is required for the SLA, since it deviates from the typical CP
# empty => null semantics
if key == 'service_level_agreement':
value = self.contents.get(key, None)
self.contents[key] = ''
else:
value = self.contents.pop(key, None)
return value is not None
def set(self, key, value):
"""
Set a key (syspurpose parameter) to value
:param key: The parameter of the syspurpose file to set
:type key: str
:param value: The value to set that parameter to
:return: Whether any change was made
"""
value = make_utf8(value)
key = make_utf8(key)
org = make_utf8(self.contents.get(key, None))
self.contents[key] = value
return org != value or org is None
def write(self, fp=None):
"""
Write the current contents to the file at self.path
"""
if not fp:
with io.open(self.path, 'w', encoding='utf-8') as f:
write_to_file_utf8(f, self.contents)
f.flush()
else:
write_to_file_utf8(fp, self.contents)
@classmethod
def read(cls, path, raise_on_error=False):
"""
Read the file represented by path. If the file does not exist it is created.
:param path: The path on the file system to read, should be a json file
:param raise_on_error: When it is set to True, then exceptions are raised as expected.
:return: new SyspurposeStore with the contents read in
"""
new_store = cls(path, raise_on_error=raise_on_error)
if not os.access(path, os.W_OK):
new_store.create()
else:
new_store.read_file()
return new_store
class SyncResult(object):
"""
A container class for the results of a sync operation performed by a SyncedStore class.
"""
def __init__(self, result, remote_changed, local_changed, cached_changed):
self.result = result
self.remote_changed = remote_changed
self.local_changed = local_changed
self.cached_changed = cached_changed
class SyncedStore(object):
"""
Stores values in a local file backed by a cache which is then synced with another source
of the same values.
"""
PATH = USER_SYSPURPOSE
CACHE_PATH = CACHED_SYSPURPOSE
def __init__(self, uep, on_changed=None, consumer_uuid=None, use_valid_fields=False):
"""
Initialization of SyncedStore
:param uep: object representing connection to candlepin server
:param on_changed: optional callback method called, during three-way merge
:param consumer_uuid: UUID of consumer
:param use_valid_fields: if valid fields are considered
"""
self.uep = uep
self.filename = self.PATH.split('/')[-1]
self.path = self.PATH
self.cache_path = self.CACHE_PATH
self.local_file = None
self.local_contents = self.get_local_contents()
self.cache_file = None
self.cache_contents = self.get_cached_contents()
self.changed = False
self.on_changed = on_changed
self.consumer_uuid = consumer_uuid
if use_valid_fields is True:
self.valid_fields = self.get_valid_fields()
else:
self.valid_fields = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.finish()
def finish(self):
"""
When local content was changed, then try to synchronize local content with remote server
:return:
"""
if self.changed:
self.sync()
def sync(self):
"""
Try to synchronize local content with remote server
:return: instance of SyncResult holding result of synchronization
"""
log.debug('Attempting to sync syspurpose content...')
try:
if self.uep and not self.uep.has_capability('syspurpose'):
log.debug('Server does not support syspurpose, syncing only locally.')
return self._sync_local_only()
except Exception as err:
log.debug(
'Failed to detect whether the server has syspurpose capability: {err}'.format(
err=err
)
)
return self._sync_local_only()
remote_contents = self.get_remote_contents()
local_contents = self.get_local_contents()
cached_contents = self.get_cached_contents()
result = self.merge(local=local_contents,
remote=remote_contents,
base=cached_contents)
local_result = {key: result[key] for key in result if result[key]}
sync_result = SyncResult(
result,
(remote_contents == result) or self.update_remote(result),
self.update_local(local_result),
self.update_cache(result),
)
log.debug('Successfully synced system purpose.')
# Reset the changed attribute as all items should be synced if we've gotten to this point
self.changed = False
return sync_result
def _sync_local_only(self):
local_updated = self.update_local(self.get_local_contents())
return SyncResult(self.local_contents, False, local_updated, False)
def merge(self, local=None, remote=None, base=None):
"""
Do three-way merge
:param local: dictionary with local values (syspyrpose.json)
:param remote: dictionary with values from server
:param base:
:return:
"""
result = three_way_merge(
local=local,
base=base,
remote=remote,
on_change=self.on_changed
)
return result
def get_local_contents(self):
"""
Try to load local content from file
:return: dictionary with system purpose values
"""
try:
self.local_contents = json.load(io.open(self.path, 'r', encoding='utf-8'))
except (os.error, ValueError, IOError):
log.debug('Unable to read local system purpose at "%s"' % self.path)
self.update_local({})
self.local_contents = {}
return self.local_contents
def get_remote_contents(self):
"""
Try to get remote content from server
:return: dictionary with system purpose values
"""
if self.uep is None or self.consumer_uuid is None:
log.debug('Failed to read remote syspurpose from server: no available connection, '
'or the consumer is not registered.')
return {}
if not self.uep.has_capability('syspurpose'):
log.debug('Server does not support syspurpose, not syncing.')
return {}
consumer = self.uep.getConsumer(self.consumer_uuid)
result = {}
# Translate from the remote values to the local, filtering out items not known
for attr in ATTRIBUTES:
value = consumer.get(LOCAL_TO_REMOTE[attr])
result[attr] = value
log.debug('Successfully read remote syspurpose from server.')
return result
def get_cached_contents(self):
"""
Try to load cached server response from the file
:return: dictionary with system purpose values
"""
try:
self.cache_contents = json.load(io.open(self.cache_path, 'r', encoding='utf-8'))
log.debug('Successfully read cached syspurpose contents.')
except (ValueError, os.error, IOError):
log.debug('Unable to read cached syspurpose contents at \'%s\'.' % self.path)
self.cache_contents = {}
self.update_cache({})
return self.cache_contents
def update_local(self, data):
"""
Rewrite local content with new data and write data to file syspurpose.json
:param data: new dictionary with local data
:return: None
"""
self.local_contents = data
self._write_local()
def _write_local(self):
"""
Write local data to the file
:return: None
"""
self._update_file(self.path, self.local_contents)
def update_cache(self, data):
self.cache_contents = data
self._write_cache()
def _write_cache(self):
"""
Write cache to file
:return: None
"""
self._update_file(self.cache_path, self.cache_contents)
def update_remote(self, data):
if self.uep is None or self.consumer_uuid is None:
log.debug('Failed to update remote syspurpose on the server: no available connection, '
'or the consumer is not registered.')
return False
addons = data.get(ADDONS)
self.uep.updateConsumer(
self.consumer_uuid,
role=data.get(ROLE) or "",
addons=addons if addons is not None else [],
service_level=data.get(SERVICE_LEVEL) or "",
usage=data.get(USAGE) or ""
)
log.debug('Successfully updated remote syspurpose on the server.')
return True
def _check_key_value_validity(self, key, value):
"""
Check validity of provided key and value of it is included in valid fields
:param key: provided key
:param value: provided value
:return: None
"""
if self.valid_fields is not None:
if key in self.valid_fields:
if value not in self.valid_fields[key]:
print(
_('Warning: Provided value "{val}" is not included in the list '
'of valid values for attribute {attr}:').format(val=value, attr=key)
)
for valid_value in self.valid_fields[key]:
if len(valid_value) > 0:
print(" - %s" % valid_value)
else:
print(_('Warning: Provided key "{key}" is not included in the list of valid keys:').format(
key=key
))
for valid_key in self.valid_fields.keys():
print(" - %s" % valid_key)
def add(self, key, value):
"""
Add a value to a list of values specified by key. If the current value specified by the key is scalar/non-list,
it is not overridden, but maintained in the list, along with the new value.
:param key: The name of the list
:param value: The value to append to the list
:return: None
"""
value = make_utf8(value)
key = make_utf8(key)
try:
# When existing value was set using set() method, then the
# existing valus is not list, but simple value. We have to convert
# it first
current_value = self.local_contents[key]
if current_value is not None and not isinstance(current_value, list):
self.local_contents[key] = [current_value]
# When existing value is None, then first covert to empty list to be
# able to call append method. It is very theoretical case.
if self.local_contents[key] is None:
self.local_contents[key] = []
if value not in self.local_contents[key]:
self.local_contents[key].append(value)
else:
log.debug('Will not add value \'%s\' to key \'%s\'.' % (value, key))
self.changed = False
return self.changed
except (AttributeError, KeyError):
self.local_contents[key] = [value]
self._check_key_value_validity(key, value)
self.changed = True
log.debug('Adding value \'%s\' to key \'%s\'.' % (value, key))
# Write changes to the syspurpose.json file
if self.changed is True:
self._write_local()
return self.changed
def remove(self, key, value):
"""
Remove a value from a list specified by key.
If the current value specified by the key is not a list, unset the value.
:param key: The name of the list parameter to manipulate
:param value: The value to attempt to remove
:return: True if the value was in the list, False if it was not
"""
value = make_utf8(value)
key = make_utf8(key)
try:
current_values = self.local_contents[key]
if current_values is not None and not isinstance(current_values, list) and current_values == value:
return self.unset(key)
if value in current_values:
self.local_contents[key].remove(value)
self.changed = True
log.debug('Removing value \'%s\' from key \'%s\'.' % (value, key))
else:
self.changed = False
log.debug('Will not remove value \'%s\' from key \'%s\'.' % (value, key))
return self.changed
except (AttributeError, KeyError, ValueError):
log.debug('Will not remove value \'%s\' from key \'%s\'.' % (value, key))
self.changed = False
# Write changes to the syspurpose.json file
if self.changed is True:
self._write_local()
return self.changed
def unset(self, key):
"""
Unsets a key
:param key: The key to unset
:return: boolean
"""
key = make_utf8(key)
# Special handling is required for the SLA, since it deviates from the typical CP
# empty => null semantics
if key == 'service_level_agreement':
value = self.local_contents.get(key, None)
self.local_contents[key] = ''
elif key == 'addons':
value = self.local_contents.get(key, None)
self.local_contents[key] = []
else:
value = self.local_contents.pop(key, None)
self.changed = True
log.debug('Unsetting value \'%s\' of key \'%s\'.' % (value, key))
self.changed = value is not None
# Write changes to the syspurpose.json file
if self.changed is True:
self._write_local()
return self.changed
def set(self, key, value):
"""
Set a key (syspurpose parameter) to value
:param key: The parameter of the syspurpose file to set
:type key: str
:param value: The value to set that parameter to
:return: Whether any change was made
"""
value = make_utf8(value)
key = make_utf8(key)
current_value = make_utf8(self.local_contents.get(key, None))
self.local_contents[key] = value
if current_value != value or current_value is None:
self._check_key_value_validity(key, value)
self.changed = True
log.debug('Setting value \'%s\' to key \'%s\'.' % (value, key))
else:
log.debug('NOT Setting value \'%s\' to key \'%s\'.')
self.changed = current_value != value or current_value is None
# Write changes to the syspurpose.json file
if self.changed is True:
self._write_local()
return self.changed
@staticmethod
def _create_missing_dir(dir_path):
"""
Try to create missing directory
:param dir_path: path to directory
:return: None
"""
# Check if the directory exists
if not os.path.isdir(dir_path):
log.debug('Trying to create directory: %s' % dir_path)
try:
os.makedirs(dir_path, mode=0o755, exist_ok=True)
except Exception as err:
log.warning('Unable to create directory: %s, error: %s' % (dir_path, err))
@classmethod
def _update_file(cls, path, data):
"""
Write the contents of data to file in the first mode we can (effectively to create or update
the file)
:param path: The string path to the file location we should update
:param data: The data to write to the file
:return: None
"""
# Check if /etc/rhsm/syspurpose directory exists
cls._create_missing_dir(USER_SYSPURPOSE_DIR)
# Check if /var/lib/rhsm/cache/ directory exists
cls._create_missing_dir(CACHE_DIR)
# Then we can try to create syspurpose.json file
try:
f = io.open(path, 'w+', encoding='utf-8')
except OSError as e:
if e.errno != 17:
raise
else:
write_to_file_utf8(f, data)
f.flush()
f.close()
log.debug('Successfully updated syspurpose values at \'%s\'.' % path)
log.debug('Failed to update syspurpose values at \'%s\'.' % path)
def get_valid_fields(self):
"""
Try to get valid fields from server using current owner (organization)
:return: Dictionary with valid fields
"""
valid_fields = None
if self.uep is not None and self.consumer_uuid is not None:
current_owner = self.uep.getOwner(self.consumer_uuid)
if 'key' in current_owner:
owner_key = current_owner['key']
try:
response = self.uep.getOwnerSyspurposeValidFields(owner_key)
except Exception as err:
log.debug("Unable to get valid fields from server: %s" % err)
else:
if 'systemPurposeAttributes' in response:
response = post_process_received_data(response)
valid_fields = response['systemPurposeAttributes']
return valid_fields
# A simple container class used to hold the values representing a change detected
# during three_way_merge
DiffChange = collections.namedtuple(
'DiffChange',
['key', 'previous_value', 'new_value', 'source', 'in_base', 'in_result']
)
def three_way_merge(local, base, remote, on_conflict="remote", on_change=None):
"""
Performs a three-way merge on the local and remote dictionaries with a given base.
:param local: The dictionary of the current local values
:param base: The dictionary with the values we've last seen
:param remote: The dictionary with "their" values
:param on_conflict: Either "remote" or "local" or None. If "remote", the remote changes
will win any conflict. If "local", the local changes will win any
conflict. If anything else, an error will be thrown.
:param on_change: This is an optional function which will be given each change as it is
detected.
:return: The dictionary of values as merged between the three provided dictionaries.
"""
log.debug('Attempting a three-way merge...')
result = {}
local = local or {}
base = base or {}
remote = remote or {}
if on_conflict == "remote":
winner = remote
elif on_conflict == "local":
winner = local
else:
raise ValueError('keyword argument "on_conflict" must be either "remote" or "local"')
if on_change is None:
on_change = lambda change: change
all_keys = set(local.keys()) | set(base.keys()) | set(remote.keys())
for key in all_keys:
local_changed = detect_changed(base=base, other=local, key=key, source="local")
remote_changed = detect_changed(base=base, other=remote, key=key, source="server")
changed = local_changed or remote_changed and remote_changed != UNSUPPORTED
source = 'base'
if local_changed == remote_changed:
if local_changed is True:
log.debug('Three way merge conflict: both local and remote values changed for key \'%s\'.' % key)
source = on_conflict
if key in winner:
result[key] = winner[key]
elif remote_changed is True:
log.debug('Three way merge: remote value was changed for key \'%s\'.' % key)
source = 'remote'
if key in remote:
result[key] = remote[key]
elif local_changed or remote_changed == UNSUPPORTED:
if local_changed is True:
log.debug('Three way merge: local value was changed for key \'%s\'.' % key)
source = 'local'
if key in local:
result[key] = local[key]
if changed:
original = base.get(key)
diff = DiffChange(key=key, source=source, previous_value=original,
new_value=result.get(key), in_base=key in base,
in_result=key in result)
on_change(diff)
return result
def detect_changed(base, other, key, source="server"):
"""
Detect the type of change that has occurred between base and other for a given key.
:param base: The dictionary of values we are starting with
:param other: The dictionary of now current values
:param key: The key that we are interested in knowing how it changed
:param source: An optional string which indicates where the "other" values came from. Used to
make decisions which are one sided. (i.e. only applicable for changes from the
server side).
:return: True if there was a change, false if there was no change
:rtype: bool
"""
base = base or {}
other = other or {}
if key not in other and source != "local":
return UNSUPPORTED
base_val = base.get(key)
other_val = other.get(key)
if key not in other and source == "local":
# If the local values no longer contain the key we want to treat this as removal
# It would constitute a change if the base had a truthy value. The values tracked from the
# server all have falsey values.
return bool(base_val)
# Handle "addons" (the lists might be out of order from the server)
if type(base_val) == list and type(other_val) == list:
return sorted(base_val) != sorted(other_val)
# When value is removed from server, then it is set to empty string, but
# it is completely removed from local syspurpose.json.
# See: https://bugzilla.redhat.com/show_bug.cgi?id=1738764
if source == "server" and base_val is None and other_val == '':
return False
return base_val != other_val
Hacked By AnonymousFox1.0, Coded By AnonymousFox