Share settings between modules

This patch change menu modules is such way that they share settings and
change it in memory not on file system. Settings dumped on fs only when
save called explicitly by user from UI.

Partial-Bug: #1527111
Change-Id: If2930991501b1718174d1b84b0d8d53af6f6a789
This commit is contained in:
Nikita Zubkov 2016-02-12 11:25:14 +03:00
parent 2aa3d81bc7
commit 42f24e8aec
17 changed files with 275 additions and 398 deletions

View File

@ -30,7 +30,7 @@ from fuelmenu.common import dialog
from fuelmenu.common import network
import fuelmenu.common.urwidwrapper as widget
from fuelmenu.common import utils
from fuelmenu import settings
from fuelmenu import settings as settings_module
log = logging.getLogger('fuelmenu.modulehelper')
@ -89,40 +89,31 @@ class ModuleHelper(object):
settings[part1] = value
@classmethod
def load(cls, modobj, ignoredparams=None):
"""Returns settings found in settings files that are found in class
def load_to_defaults(cls, settings, defaults, ignoredparams=None):
"""Update module defaults by appropriate values from settings.
:param cls: ModuleHelper object
:param modobj: object from calling class
:param ignoredparams: list of parameters to skip lookup from settings
:returns: OrderedDict of settings for calling class
settings: Settings object
defaults: module object's defaults from calling class
ignoredparams: list of parameters to skip lookup from settings
"""
# Read in yaml
defaultsettings = settings.Settings().read(
modobj.parent.defaultsettingsfile)
usersettings = settings.Settings().read(modobj.parent.settingsfile)
oldsettings = utils.dict_merge(defaultsettings, usersettings)
types_to_skip = (WidgetType.BUTTON, WidgetType.LABEL)
for setting, setting_def in six.iteritems(modobj.defaults):
for setting, setting_def in six.iteritems(defaults):
if (setting_def.get('type') in types_to_skip or
ignoredparams and setting in ignoredparams):
continue
try:
setting_def["value"] = cls.get_setting(oldsettings, setting)
setting_def["value"] = cls.get_setting(settings, setting)
except KeyError:
log.warning("Failed to load %s value from settings", setting)
return oldsettings
@classmethod
def save(cls, modobj, responses):
newsettings = collections.OrderedDict()
def make_settings_from_responses(cls, responses):
"""Create new Settings object from responses."""
newsettings = settings_module.Settings()
for setting in responses:
cls.set_setting(newsettings,
setting,
responses[setting],
modobj.oldsettings)
cls.set_setting(newsettings, setting, responses[setting])
return newsettings
@classmethod

View File

@ -11,7 +11,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import logging
import random as _random
import string
@ -23,44 +22,6 @@ from fuelmenu import consts
log = logging.getLogger('fuelmenu.common.utils')
random = _random.SystemRandom()
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
def dict_merge(a, b):
"""Recursively merges values in dicts from b into a
All values in b override a, even if b is not a dict:
Example:
x = {'a': {'b' : 'val'}}
y = {'a': 'notval'}
z = {'z': None}
dict_merge(x, y) returns {'a': 'notval'}
dict_merge(x, z) returns {'a': {'b': 'val'}, {'z': None}}
dict_merge(z, x) returns {'z': None, 'a': {'b': 'val'}}
:param a: the first dict
:param b: any value
:returns: resulting value of b merged into a, with b taking precedence
"""
if not isinstance(a, (dict, OrderedDict)):
raise TypeError('First parameter is not a dict')
result = copy.deepcopy(a)
try:
for k, v in b.iteritems():
if k in result and isinstance(result[k],
(dict, OrderedDict)):
result[k] = dict_merge(result[k], v)
else:
result[k] = copy.deepcopy(v)
except AttributeError:
# Non-iterable objects should be just returned
return b
return result
def get_deployment_mode():
"""Report if any fuel containers are already created."""

View File

@ -14,6 +14,7 @@
# under the License.
from __future__ import absolute_import
from fuelmenu.common import dialog
from fuelmenu.common import errors
from fuelmenu.common import network
@ -21,7 +22,9 @@ from fuelmenu.common import timeout
from fuelmenu.common import urwidwrapper as widget
from fuelmenu.common import utils
from fuelmenu import consts
from fuelmenu.settings import Settings
from fuelmenu import settings as settings_module
import logging
import operator
from optparse import OptionParser
@ -83,13 +86,22 @@ class FuelSetup(object):
self.footer = None
self.frame = None
self.screen = None
self.defaultsettingsfile = os.path.join(os.path.dirname(__file__),
"settings.yaml")
self.settingsfile = consts.SETTINGS_FILE
self.managediface = network.get_physical_ifaces()[0]
# Set to true to move all settings to end
self.globalsave = True
self.version = utils.get_fuel_version()
# settings load
self.settings = settings_module.Settings()
self.settings.load(
os.path.join(os.path.dirname(__file__), "settings.yaml"),
template_kwargs={"mos_version": self.version})
self.settings.load(
consts.SETTINGS_FILE,
template_kwargs={"mos_version": self.version})
self.main()
self.choices = []
@ -304,6 +316,8 @@ class FuelSetup(object):
except AttributeError as e:
log.debug("Module %s does not have save function: %s"
% (modulename, e))
self.settings.write(outfn=consts.SETTINGS_FILE)
return True, None
@ -370,12 +384,15 @@ def save_only(iface, settingsfile=consts.SETTINGS_FILE):
default_settings_file = os.path.join(os.path.dirname(__file__),
"settings.yaml")
mos_version = utils.get_fuel_version()
settings = Settings().read(
settings = settings_module.Settings()
settings.load(
default_settings_file,
template_kwargs={"mos_version": mos_version})
settings.update(Settings().read(
settingsfile,
template_kwargs={"mos_version": mos_version}))
settings.load(settingsfile, template_kwargs={"mos_version": mos_version})
settings_upd = \
{
"ADMIN_NETWORK/interface": iface,
@ -429,8 +446,7 @@ def save_only(iface, settingsfile=consts.SETTINGS_FILE):
settings[setting] = settings_upd[setting]
# Write astute.yaml
Settings().write(settings, defaultsfile=default_settings_file,
outfn=settingsfile)
settings.write(outfn=settingsfile)
def main(*args, **kwargs):

View File

@ -27,7 +27,6 @@ from fuelmenu.common.modulehelper import BLANK_KEY
from fuelmenu.common.modulehelper import ModuleHelper
from fuelmenu.common.modulehelper import WidgetType
from fuelmenu.common import utils
from fuelmenu.settings import Settings
log = logging.getLogger('fuelmenu.mirrors')
blank = urwid.Divider()
@ -49,7 +48,6 @@ class bootstrapimg(urwid.WidgetWrap):
self.priority = 55
self.visible = True
self.parent = parent
self._mos_version = None
# UI Text
self.header_content = ["Bootstrap image configuration"]
@ -117,15 +115,9 @@ class bootstrapimg(urwid.WidgetWrap):
"callback": self.add_repo
}
}
self.oldsettings = self.load()
self.load()
self.screen = None
@property
def mos_version(self):
if not self._mos_version:
self._mos_version = utils.get_fuel_version()
return self._mos_version
@property
def responses(self):
ret = dict()
@ -311,7 +303,6 @@ class bootstrapimg(urwid.WidgetWrap):
return repos_for_ui
def add_repo(self, data=None):
defaults = self._get_fresh_defaults()
repo_list = defaults[BOOTSTRAP_REPOS_KEY]['value']
repo_list.append(
@ -335,38 +326,18 @@ class bootstrapimg(urwid.WidgetWrap):
log.warning("unexpected error: {0}".format(e))
def load(self):
# Read in yaml
default_settings = Settings().read(
self.parent.defaultsettingsfile,
template_kwargs={"mos_version": self.mos_version})
settings = default_settings
settings.update(Settings().read(self.parent.settingsfile))
settings = self.parent.settings
ModuleHelper.load_to_defaults(settings, self.defaults)
self._update_defaults(self.defaults, settings)
self._select_fields_to_show(self.defaults)
return settings
def _make_settings_from_responses(self, responses):
settings = dict()
for setting in responses:
new_value = responses[setting]
ModuleHelper.set_setting(settings, setting, new_value,
self.oldsettings)
return settings
def save(self, responses):
newsettings = ModuleHelper.make_settings_from_responses(responses)
self.parent.settings.merge(newsettings)
newsettings = self._make_settings_from_responses(responses)
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
# Set oldsettings to reflect new settings
self.oldsettings = newsettings
# Update self.defaults
self._update_defaults(self.defaults, newsettings)
self._update_defaults(self.defaults, self.parent.settings)
def check_url(self, url, proxies):
try:
@ -401,7 +372,7 @@ class bootstrapimg(urwid.WidgetWrap):
def _get_fresh_defaults(self):
defaults = copy.copy(self.defaults)
self._update_defaults(defaults,
self._make_settings_from_responses(
ModuleHelper.make_settings_from_responses(
self.responses))
return defaults

View File

@ -20,7 +20,6 @@ from fuelmenu.common.modulehelper import WidgetType
from fuelmenu.common import network
import fuelmenu.common.urwidwrapper as widget
from fuelmenu.common import utils
from fuelmenu.settings import Settings
import logging
import netaddr
import urwid
@ -78,8 +77,8 @@ to advertise via DHCP to nodes",
"type": WidgetType.LABEL},
}
self.load()
self.extdhcp = True
self.oldsettings = self.load()
self.screen = None
def check(self, args):
@ -246,14 +245,16 @@ interface first.")
# Extra checks for post-deployment changes
if utils.is_post_deployment():
settings = self.parent.settings
# Admin interface cannot change
if self.activeiface != \
self.oldsettings["ADMIN_NETWORK"]["interface"]:
if self.activeiface != settings["ADMIN_NETWORK"]["interface"]:
errors.append("Cannot change admin interface after deployment")
# PXE network range must contain previous PXE network range
old_range = network.range(
self.oldsettings["ADMIN_NETWORK"]["dhcp_pool_start"],
self.oldsettings["ADMIN_NETWORK"]["dhcp_pool_end"])
settings["ADMIN_NETWORK"]["dhcp_pool_start"],
settings["ADMIN_NETWORK"]["dhcp_pool_end"])
new_range = network.range(
responses["ADMIN_NETWORK/dhcp_pool_start"],
responses["ADMIN_NETWORK/dhcp_pool_end"])
@ -286,42 +287,27 @@ interface first.")
self.setNetworkDetails()
def load(self):
oldsettings = ModuleHelper.load(self)
if oldsettings["ADMIN_NETWORK"]["interface"] \
in self.netsettings.keys():
self.activeiface = oldsettings["ADMIN_NETWORK"]["interface"]
return oldsettings
settings = self.parent.settings
ModuleHelper.load_to_defaults(settings, self.defaults)
iface = settings["ADMIN_NETWORK"]["interface"]
if iface in self.netsettings.keys():
self.activeiface = iface
def save(self, responses):
# Generic settings start ##
newsettings = ModuleHelper.save(self, responses)
for setting in responses.keys():
if "/" in setting:
part1, part2 = setting.split("/")
if part1 not in newsettings:
# We may not touch all settings, so copy oldsettings first
newsettings[part1] = self.oldsettings[part1]
newsettings[part1][part2] = responses[setting]
else:
newsettings[setting] = responses[setting]
# Generic settings end
newsettings = ModuleHelper.make_settings_from_responses(responses)
# Need to calculate and netmask
newsettings['ADMIN_NETWORK']['netmask'] = \
self.netsettings[newsettings['ADMIN_NETWORK']['interface']][
"netmask"]
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
# Set oldsettings to reflect new settings
self.oldsettings = newsettings
# Update self.defaults
for index, fieldname in enumerate(self.fields):
if fieldname != "blank" and "label" not in fieldname:
self.defaults[fieldname]['value'] = responses[fieldname]
self.parent.settings.merge(newsettings)
self.parent.footer.set_text("Changes saved successfully.")
def getNetwork(self):

View File

@ -19,7 +19,7 @@ from fuelmenu.common import network
from fuelmenu.common import replace
import fuelmenu.common.urwidwrapper as widget
from fuelmenu.common import utils
from fuelmenu.settings import Settings
import logging
import netaddr
import os
@ -73,7 +73,7 @@ DNS (space separated)",
is accessible"}
}
self.oldsettings = self.load()
self.load()
self.screen = None
self.fixEtcHosts()
@ -232,7 +232,7 @@ is accessible"}
if "localhost" in line:
etchosts.write(line)
elif responses["HOSTNAME"] in line \
or self.oldsettings["HOSTNAME"] \
or self.parent.settings["HOSTNAME"] \
or self.netsettings[self.parent.managediface]['addr'] \
in line:
continue
@ -282,7 +282,7 @@ is accessible"}
# Precedence of DNS information:
# Class defaults, fuelmenu default YAML, astute.yaml, uname,
# /etc/resolv.conf
oldsettings = ModuleHelper.load(self, ignoredparams=['TEST_DNS'])
oldsettings = self.parent.settings
# Read hostname from uname
try:
@ -302,17 +302,9 @@ is accessible"}
if nameservers:
oldsettings["DNS_UPSTREAM"] = nameservers
for setting in self.defaults.keys():
try:
if "/" in setting:
part1, part2 = setting.split("/")
self.defaults[setting]["value"] = oldsettings[part1][part2]
else:
self.defaults[setting]["value"] = oldsettings[setting]
except Exception:
log.warning("No setting named %s found." % setting)
continue
return oldsettings
ModuleHelper.load_to_defaults(oldsettings,
self.defaults,
ignoredparams=['TEST_DNS'])
def getDNS(self, resolver="/etc/resolv.conf"):
nameservers = []
@ -341,26 +333,9 @@ is accessible"}
return searches, domain, ",".join(nameservers)
def save(self, responses):
# Generic settings start
newsettings = dict()
for setting in responses.keys():
if "/" in setting:
part1, part2 = setting.split("/")
if part1 not in newsettings:
# We may not touch all settings, so copy oldsettings first
newsettings[part1] = self.oldsettings[part1]
newsettings[part1][part2] = responses[setting]
else:
newsettings[setting] = responses[setting]
# Generic settings end
newsettings = ModuleHelper.make_settings_from_responses(responses)
self.parent.settings.merge(newsettings)
# log.debug(str(newsettings))
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
# Set oldsettings to reflect new settings
self.oldsettings = newsettings
# Update self.defaults
for index, fieldname in enumerate(self.fields):
if fieldname != "blank":

View File

@ -19,7 +19,6 @@ import urwid
from fuelmenu.common.modulehelper import ModuleHelper
from fuelmenu.common.modulehelper import WidgetType
from fuelmenu.settings import Settings
log = logging.getLogger(__name__)
@ -54,7 +53,7 @@ class feature_groups(urwid.WidgetWrap):
"type": WidgetType.CHECKBOX,
}
}
self.oldsettings = self.load()
self.load()
self.screen = None
@property
@ -81,34 +80,23 @@ class feature_groups(urwid.WidgetWrap):
def load(self):
# Read in yaml
defaultsettings = Settings().read(self.parent.defaultsettingsfile)
oldsettings = defaultsettings
oldsettings.update(Settings().read(self.parent.settingsfile))
oldsettings = self.parent.settings
for setting in self.defaults:
try:
part1, part2 = setting.split("/")
self.defaults[setting]["value"] = part2 in oldsettings[part1]
except Exception as e:
log.warning("unexpected error: %s", e.message)
return oldsettings
def save(self, responses):
newsettings = {}
for setting in responses.keys():
part1, part2 = setting.split("/")
if part1 not in newsettings:
newsettings[part1] = []
if responses[setting]:
newsettings[part1].append(part2)
settings = self.parent.settings
newsettings = ModuleHelper.make_settings_from_responses(responses)
settings.merge(newsettings)
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
self.oldsettings = newsettings
for setting in self.defaults:
part1, part2 = setting.split("/")
self.defaults[setting]["value"] = part2 in newsettings[part1]
self.defaults[setting]["value"] = part2 in settings[part1]
def cancel(self, button):
ModuleHelper.cancel(self, button)

View File

@ -13,14 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
try:
from collections import OrderedDict
except Exception:
# python 2.6 or earlier use backport
from ordereddict import OrderedDict
from fuelmenu.common.modulehelper import ModuleHelper
from fuelmenu.settings import Settings
import logging
import re
import urwid
@ -58,7 +51,7 @@ class fueluser(urwid.WidgetWrap):
"value": ""},
}
self.oldsettings = self.load()
self.load()
self.screen = None
def check(self, args):
@ -139,33 +132,18 @@ class fueluser(urwid.WidgetWrap):
return True
def save(self, responses):
# Generic settings start
newsettings = OrderedDict()
for setting in responses.keys():
if "/" in setting:
part1, part2 = setting.split("/")
if part1 not in newsettings:
# We may not touch all settings, so copy oldsettings first
try:
newsettings[part1] = self.oldsettings[part1]
except Exception:
if part1 not in newsettings.keys():
newsettings[part1] = OrderedDict()
log.warning("issues setting newsettings %s " % setting)
log.warning("current newsettings: %s" % newsettings)
newsettings[part1][part2] = responses[setting]
else:
newsettings[setting] = responses[setting]
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
newsettings = ModuleHelper.make_settings_from_responses(responses)
self.parent.settings.merge(newsettings)
self.parent.footer.set_text("Changes applied successfully.")
# Reset fields
self.cancel(None)
def load(self):
return ModuleHelper.load(self, ignoredparams=['CONFIRM_PASSWORD'])
ModuleHelper.load_to_defaults(
self.parent.settings,
self.defaults,
ignoredparams=['CONFIRM_PASSWORD'])
def cancel(self, button):
ModuleHelper.cancel(self, button)

View File

@ -18,7 +18,6 @@ from fuelmenu.common.modulehelper import ModuleHelper
from fuelmenu.common.modulehelper import WidgetType
import fuelmenu.common.urwidwrapper as widget
from fuelmenu.common import utils
from fuelmenu.settings import Settings
import logging
import re
import urwid
@ -63,7 +62,7 @@ class ntpsetup(urwid.WidgetWrap):
# Load info
self.gateway = self.get_default_gateway_linux()
self.oldsettings = self.load()
self.load()
self.screen = None
def check(self, args):
@ -176,32 +175,18 @@ class ntpsetup(urwid.WidgetWrap):
return ModuleHelper.get_default_gateway_linux()
def load(self):
return ModuleHelper.load(self, ignoredparams=['ntpenabled'])
ModuleHelper.load_to_defaults(
self.parent.settings, self.defaults, ignoredparams=['ntpenabled'])
def save(self, responses):
# Generic settings start
newsettings = dict()
for setting in responses.keys():
if "/" in setting:
part1, part2 = setting.split("/")
if part1 not in newsettings:
# We may not touch all settings, so copy oldsettings first
newsettings[part1] = self.oldsettings[part1]
newsettings[part1][part2] = responses[setting]
else:
newsettings[setting] = responses[setting]
# Generic settings end
settings = self.parent.settings
newsettings = ModuleHelper.make_settings_from_responses(responses)
settings.merge(newsettings)
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
# Set oldsettings to reflect new settings
self.oldsettings = newsettings
# Update defaults
for index, fieldname in enumerate(self.fields):
if fieldname != "blank" and fieldname in newsettings:
self.defaults[fieldname]['value'] = newsettings[fieldname]
if fieldname != "blank" and fieldname in settings:
self.defaults[fieldname]['value'] = settings[fieldname]
def checkNTP(self, server):
# Use ntpdate to verify server answers NTP requests

View File

@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import logging
import os
@ -91,7 +90,7 @@ class restore(urwid.WidgetWrap):
},
}
self.oldsettings = self.load()
self.load()
def cancel(self, button):
helper.ModuleHelper.cancel(self, button)
@ -101,7 +100,7 @@ class restore(urwid.WidgetWrap):
def check_settings(self, settings):
required_keys = []
responses = collections.OrderedDict()
responses = settings_utils.Settings()
for key, subkeys in KEYS_TO_RESTORE:
if key not in settings:
if subkeys:
@ -139,8 +138,8 @@ class restore(urwid.WidgetWrap):
path = os.path.abspath(path)
try:
with open(path) as f:
settings = yaml.load(f)
settings = settings_utils.Settings()
settings.load(path)
except IOError as err:
self.show_error_msg("Could not fetch settings: {0}".format(err),
exc_info=True)
@ -181,22 +180,11 @@ class restore(urwid.WidgetWrap):
return True
def load(self):
return helper.ModuleHelper.load(self, ignoredparams=('PATH',))
helper.ModuleHelper.load_to_defaults(
self.parent.settings, self.defaults, ignoredparams=('PATH',))
def save(self, responses):
newsettings = helper.ModuleHelper.save(self, responses)
# TODO(akscram): The restore module writes settings itself into
# the configuration file and requires from the
# user to exit from the menu without saving
# changes. It is a necessary requirement due to
# the limitations of fuel menu. For more
# information see the bug report
# https://bugs.launchpad.net/fuel/+bug/1527111.
settings_utils.Settings().write(
newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
self.oldsettings = newsettings
self.parent.settings.merge(responses)
def screenUI(self):
return helper.ModuleHelper.screenUI(

View File

@ -16,7 +16,6 @@
import crypt
from fuelmenu.common import modulehelper as helper
from fuelmenu.common import utils
from fuelmenu import settings as settings_module
import logging
import urwid
@ -123,13 +122,10 @@ class rootpw(urwid.WidgetWrap):
return True
def save_settings(self, hashed_pwd):
bootstrap = helper.ModuleHelper.load(self)['BOOTSTRAP']
bootstrap['hashed_root_password'] = hashed_pwd
settings_module.Settings().write(
{'BOOTSTRAP': bootstrap},
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
newsettings = {'BOOTSTRAP': {
'hashed_root_password': hashed_pwd,
}}
self.parent.settings.merge(newsettings)
def cancel(self, button):
helper.ModuleHelper.cancel(self, button)

View File

@ -13,14 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
try:
from collections import OrderedDict
except Exception:
# python 2.6 or earlier use backport
from ordereddict import OrderedDict
from fuelmenu.common.modulehelper import ModuleHelper
from fuelmenu.common import pwgen
from fuelmenu.settings import Settings
import logging
import urwid
import urwid.raw_display
@ -116,7 +110,7 @@ class servicepws(urwid.WidgetWrap):
}
self.fields = self.defaults.keys()
self.oldsettings = self.load()
self.load()
self.screen = None
def check(self, args):
@ -144,35 +138,13 @@ class servicepws(urwid.WidgetWrap):
self.save(responses)
def load(self):
return ModuleHelper.load(self)
ModuleHelper.load_to_defaults(self.parent.settings, self.defaults)
def save(self, responses):
# Generic settings start
newsettings = OrderedDict()
for setting in responses.keys():
if "/" in setting:
part1, part2 = setting.split("/")
if part1 not in newsettings:
# We may not touch all settings, so copy oldsettings first
try:
newsettings[part1] = self.oldsettings[part1]
except Exception:
if part1 not in newsettings.keys():
newsettings[part1] = OrderedDict()
log.warning("issues setting newsettings %s " % setting)
log.warning("current newsettings: %s" % newsettings)
newsettings[part1][part2] = responses[setting]
else:
newsettings[setting] = responses[setting]
Settings().write(newsettings,
defaultsfile=self.parent.defaultsettingsfile,
outfn=self.parent.settingsfile)
# Generic settings end
newsettings = ModuleHelper.make_settings_from_responses(responses)
self.parent.settings.merge(newsettings)
log.debug('done saving servicepws')
# Set oldsettings to reflect new settings
self.oldsettings = newsettings
# Update defaults
for index, fieldname in enumerate(self.fields):
if fieldname != "blank" and fieldname in newsettings:

View File

@ -13,6 +13,7 @@
# under the License.
import collections
import copy
import logging
from string import Template
@ -24,6 +25,8 @@ except Exception:
import yaml
log = logging.getLogger('fuelmenu.settings')
def construct_ordered_mapping(self, node, deep=False):
if not isinstance(node, yaml.MappingNode):
@ -76,29 +79,74 @@ def represent_ordered_mapping(self, tag, mapping, flow_style=None):
else:
node.flow_style = best_style
return node
# Settings object is the instance of OrderedDict, so multi_representer
# of OrderedDict can handle both types (OrderedDict and Settings)
yaml.representer.Representer.add_multi_representer(
OrderedDict, yaml.representer.SafeRepresenter.represent_dict)
yaml.representer.BaseRepresenter.represent_mapping = represent_ordered_mapping
yaml.representer.Representer.add_representer(OrderedDict, yaml.representer.
SafeRepresenter.represent_dict)
class Settings(object):
def read(self, yamlfile, template_kwargs=None):
def dict_merge(a, b):
"""Recursively merges values in dicts from b into a
All values in b override a, even if b is not a dict:
Example:
x = {'a': {'b' : 'val'}}
y = {'a': 'notval'}
z = {'z': None}
dict_merge(x, y) returns {'a': 'notval'}
dict_merge(x, z) returns {'a': {'b': 'val'}, {'z': None}}
dict_merge(z, x) returns {'z': None, 'a': {'b': 'val'}}
:param a: the first dict
:param b: any value
:returns: resulting value of b merged into a, with b taking precedence
"""
if not isinstance(a, (dict, OrderedDict)):
raise TypeError('First parameter is not a dict')
result = copy.deepcopy(a)
try:
for k, v in b.iteritems():
if k in result and isinstance(result[k],
(dict, OrderedDict)):
result[k] = dict_merge(result[k], v)
else:
result[k] = copy.deepcopy(v)
except AttributeError:
# Non-iterable objects should be just returned
return b
return result
class Settings(OrderedDict):
def load(self, settings_file, template_kwargs=None):
"""Load setting from file and merge them to existing object
settings_file: path to setings.yaml file
template_kwargs: dict with parameters that will be placed
instead labeles in settings file before yaml parsing
"""
try:
with open(yamlfile) as infile:
with open(settings_file) as infile:
settings = yaml.load(Template(
infile.read()).safe_substitute(template_kwargs or {}))
return settings or OrderedDict()
except Exception:
if yamlfile is not None:
logging.error("Unable to read YAML: %s", yamlfile)
return OrderedDict()
def write(self, newvalues, tree=None, defaultsfile='settings.yaml',
outfn='mysettings.yaml'):
settings = self.read(defaultsfile)
settings.update(self.read(outfn))
settings.update(newvalues)
self.merge(settings)
except Exception:
log.error("Unable to read YAML: %s", settings_file)
return self
def write(self, outfn='mysettings.yaml'):
"""Write settings to file."""
with open(outfn, 'w') as outfile:
yaml.dump(settings, outfile, default_style='"',
yaml.dump(self, outfile, default_style='"',
default_flow_style=False)
return True
return True
def merge(self, other):
"""Merge this settings object with other."""
self.update(dict_merge(self, other))

View File

@ -65,4 +65,4 @@ BOOTSTRAP:
suite: "mos${mos_version}-holdback"
type: "deb"
PRODUCTION: docker
FEATURE_GROUPS: []
FEATURE_GROUPS: {}

View File

@ -19,6 +19,7 @@ import netifaces
import unittest
from fuelmenu.common import modulehelper
from fuelmenu import settings as settings_module
def custom_mock_open(lines):
@ -81,7 +82,6 @@ class TestModuleHelperGet(TestModuleHelperBase):
self.settings, incorrect_key)
@mock.patch('fuelmenu.settings.Settings.read', side_effect=lambda x: x)
@mock.patch('fuelmenu.common.modulehelper.ModuleHelper.get_setting',
return_value='loaded')
class TestModuleHelperLoad(TestModuleHelperBase):
@ -89,8 +89,8 @@ class TestModuleHelperLoad(TestModuleHelperBase):
super(TestModuleHelperLoad, self).setUp()
self.modobj.defaults = dict()
self.modobj.parent = mock.Mock()
self.modobj.parent.defaultsettingsfile = {'key1': 'value1'}
self.modobj.parent.settingsfile = {'key2': 'value2'}
self.modobj.parent.settings = settings_module.Settings(
{'key1': 'value1', 'key2': 'value2'})
def test_load_types_skipping(self, *_):
widget_types = {
@ -110,8 +110,7 @@ class TestModuleHelperLoad(TestModuleHelperBase):
},
})
self._check('load', {'key2': 'value2', 'key1': 'value1'},
self.modobj)
self._run('load_to_defaults', self.modobj, self.modobj.defaults)
for _, setting in self.modobj.defaults.items():
self.assertEqual(
widget_types[setting['type']], setting['value'],
@ -125,8 +124,8 @@ class TestModuleHelperLoad(TestModuleHelperBase):
3: {'value': 'skipped', 'should': 'skipped'},
})
ignores = [1, 3]
self._check('load', {'key2': 'value2', 'key1': 'value1'},
self.modobj, ignores)
self._run('load_to_defaults', self.modobj,
self.modobj.defaults, ignores)
for _, setting in self.modobj.defaults.items():
self.assertEqual(setting['value'], setting['should'])
@ -136,32 +135,25 @@ class TestModuleHelperLoad(TestModuleHelperBase):
self, m_warning, m_get_setting, *_):
m_get_setting.side_effect = KeyError
self.modobj.defaults.update({'key': {'value': ''}})
self._check('load', {'key2': 'value2', 'key1': 'value1'},
self.modobj)
self._run('load_to_defaults', self.modobj, self.modobj.defaults)
self.assertEqual(self.modobj.defaults['key']['value'], '')
m_warning.assert_called_once_with(
"Failed to load %s value from settings", 'key')
def test_load_settings(self, *_):
self._check('load', {'key1': 'value1', 'key2': 'value2'}, self.modobj)
@mock.patch('fuelmenu.common.modulehelper.ModuleHelper.set_setting',
return_value='')
class TestModuleHelperSave(TestModuleHelperBase):
def setUp(self):
super(TestModuleHelperSave, self).setUp()
self.modobj.oldsettings = mock.Mock()
def test_save(self, m_set_setting):
def test_make_settings_from_responses(self):
responses = {
'key1': 'value1',
'key2': 'value2'
'key2/key3': 'value2'
}
self._check('save', {}, self.modobj, responses)
for key, value in responses.items():
m_set_setting.assert_any_call(
{}, key, value, self.modobj.oldsettings)
expected = {'key1': 'value1', 'key2': {'key3': 'value2'}}
self._check('make_settings_from_responses', expected, responses)
class TestModuleHelperCancel(TestModuleHelperBase):

View File

@ -23,39 +23,6 @@ import unittest
class TestUtils(unittest.TestCase):
def test_dict_merge_simple(self):
a = {'a': 1}
b = {'b': 2}
data = utils.dict_merge(a, b)
self.assertEqual({'a': 1, 'b': 2}, data)
def test_dict_merge_intended_behavior(self):
"""If b is not a dict, it is the result."""
a = {'a': 1}
b = None
data = utils.dict_merge(a, b)
self.assertEqual(None, data)
def test_dict_merge_bad_data(self):
"""If a is not a dict, it should raise TypeError."""
a = {'a': 1}
b = None
c = 1
d = (1, 2, 3)
e = set(['A', 'B', 'C'])
self.assertRaises(TypeError, utils.dict_merge, b, a)
self.assertRaises(TypeError, utils.dict_merge, c, a)
self.assertRaises(TypeError, utils.dict_merge, d, a)
self.assertRaises(TypeError, utils.dict_merge, e, a)
def test_dict_merge_override(self):
a = {'a': {'c': 'val'}}
b = {'b': 2, 'a': 'notval'}
data = utils.dict_merge(a, b)
self.assertEqual({'a': 'notval', 'b': 2}, data)
def make_process_mock(self, return_code=0, retval=('stdout', 'stderr')):
process_mock = mock.Mock(
communicate=mock.Mock(return_value=retval))

View File

@ -14,39 +14,102 @@
# License for the specific language governing permissions and limitations
# under the License.
try:
from collections import OrderedDict
except Exception:
# python 2.6 or earlier use backport
from ordereddict import OrderedDict
import os
import shutil
import tempfile
import unittest
import mock
import yaml
from fuelmenu import settings
from fuelmenu import settings as settings_module
def test_read_settings(tmpdir):
yaml_file = tmpdir.join("yamlfile.yaml")
yaml_file.write("""
class TestDictMege(unittest.TestCase):
def test_dict_merge_simple(self):
a = {'a': 1}
b = {'b': 2}
data = settings_module.dict_merge(a, b)
self.assertEqual({'a': 1, 'b': 2}, data)
def test_dict_merge_intended_behavior(self):
"""If b is not a dict, it is the result."""
a = {'a': 1}
b = None
data = settings_module.dict_merge(a, b)
self.assertEqual(None, data)
def test_dict_merge_bad_data(self):
"""If a is not a dict, it should raise TypeError."""
a = {'a': 1}
b = None
c = 1
d = (1, 2, 3)
e = {'A', 'B', 'C'}
self.assertRaises(TypeError, settings_module.dict_merge, b, a)
self.assertRaises(TypeError, settings_module.dict_merge, c, a)
self.assertRaises(TypeError, settings_module.dict_merge, d, a)
self.assertRaises(TypeError, settings_module.dict_merge, e, a)
def test_dict_merge_override(self):
a = {'a': {'c': 'val'}}
b = {'b': 2, 'a': 'notval'}
data = settings_module.dict_merge(a, b)
self.assertEqual({'a': 'notval', 'b': 2}, data)
class TestSettings(unittest.TestCase):
def setUp(self):
self.directory = tempfile.mkdtemp()
yaml_file = os.path.join(self.directory, "__yamlfile.yaml")
open(yaml_file, 'w').write("""
sample:
- one
- a: b
c: d
one:
a: b
c: d
""")
data = settings.Settings().read(yaml_file.strpath)
assert data == {
'sample': [
'one',
{
'a': 'b',
'c': 'd',
self.settings = settings_module.Settings()
self.settings.load(yaml_file)
def tearDown(self):
shutil.rmtree(self.directory, ignore_errors=True)
def test_read_settings(self):
self.assertEqual(self.settings, {
'sample': {
'one': {
'a': 'b',
'c': 'd',
}
}
]
}
assert isinstance(data, OrderedDict)
})
def test_merge_settings(self):
yaml_file = os.path.join(self.directory, "yamlfile.yaml")
open(yaml_file, 'w').write("""{sample: {one: {a: 666}}}""")
@mock.patch('fuelmenu.settings.file', side_effect=Exception('Error'))
def test_read_settings_with_error(_):
data = settings.Settings().read('some_path')
assert data == {}
assert isinstance(data, OrderedDict)
self.settings.load(yaml_file)
self.assertEqual(self.settings, {
'sample': {
'one': {
'a': 666,
'c': 'd',
}
}
})
@mock.patch('__builtin__.open', side_effect=Exception('Error'))
def test_read_settings_with_error(self, _):
data = settings_module.Settings()
data.load('some_path')
self.assertEqual(data, {})
def test_write_settings(self):
outfile = os.path.join(self.directory, 'out.yaml')
self.settings.write(outfile)
self.assertTrue(os.path.exists(outfile))
self.assertTrue(yaml.safe_load(open(outfile)) == self.settings)