#!/usr/bin/env python # Copyright 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 re import requests import types import six import urwid import urwid.raw_display import urwid.web_display from fuelmenu.common.modulehelper import BLANK_KEY from fuelmenu.common.modulehelper import ModuleHelper from fuelmenu.common.modulehelper import WidgetType from fuelmenu.common import utils log = logging.getLogger('fuelmenu.mirrors') blank = urwid.Divider() local_repo_pattern = re.compile(r'^http[s]?://(127.0.0.1|localhost)([:/].*)?$') BOOTSTRAP_HTTP_PROXY_KEY = "BOOTSTRAP/http_proxy" BOOTSTRAP_HTTPS_PROXY_KEY = "BOOTSTRAP/https_proxy" BOOTSTRAP_REPOS_KEY = "BOOTSTRAP/repos" BOOTSTRAP_SKIP_BUILD_KEY = "BOOTSTRAP/skip_default_img_build" ADD_REPO_BUTTON_KEY = 'add_repo_button' class BootstrapImage(urwid.WidgetWrap): def __init__(self, parent): self.name = "Bootstrap Image" self.visible = True self.parent = parent # UI Text self.header_content = ["Bootstrap image configuration"] self._common_fields = ( BLANK_KEY, BOOTSTRAP_SKIP_BUILD_KEY, ) self._repo_related_fields = ( BLANK_KEY, BOOTSTRAP_HTTP_PROXY_KEY, BOOTSTRAP_HTTPS_PROXY_KEY, BLANK_KEY, BOOTSTRAP_REPOS_KEY, ADD_REPO_BUTTON_KEY ) self.fields = self._common_fields self.repo_list = [] self.repo_value_scheme = { "name": { "type": WidgetType.TEXT_FIELD, "label": "Name", "tooltip": "Repository name" }, "uri": { "type": WidgetType.TEXT_FIELD, "label": "Deb repo", "tooltip": "Repo in format: " "deb uri distribution [component1] [...]" }, "priority": { "type": WidgetType.TEXT_FIELD, "label": "Priority", "tooltip": "Repository priority" } } self.defaults = { BOOTSTRAP_SKIP_BUILD_KEY: { "label": "Skip building bootstrap image", "tooltip": "", "type": WidgetType.CHECKBOX, "callback": self.skip_build_callback}, BOOTSTRAP_HTTP_PROXY_KEY: { "label": "HTTP proxy", "tooltip": "Use this proxy when building the bootstrap image", "value": ""}, BOOTSTRAP_HTTPS_PROXY_KEY: { "label": "HTTPS proxy", "tooltip": "Use this proxy when building the bootstrap image", "value": ""}, BOOTSTRAP_REPOS_KEY: { "label": "List of repositories", "type": WidgetType.LIST, "value_scheme": self.repo_value_scheme, "value": self.repo_list }, ADD_REPO_BUTTON_KEY: { "label": "Add repository", "type": WidgetType.BUTTON, "callback": self.add_repo } } self.load() self.screen = None @property def responses(self): ret = dict() for index, fieldname in enumerate(self.fields): if fieldname == BLANK_KEY or 'button' in fieldname.lower(): pass elif fieldname == BOOTSTRAP_REPOS_KEY: ret[fieldname] = \ self._get_repo_list_response(self.edits[index]) elif fieldname == BOOTSTRAP_SKIP_BUILD_KEY: ret[fieldname] = self.edits[index].get_state() else: ret[fieldname] = self.edits[index].get_edit_text() return ret def check(self, args): """Validate that all fields have valid values through sanity checks.""" self.parent.footer.set_text("Checking data...") self.parent.refreshScreen() responses = self.responses if self.parent.save_only: return responses errors = [] if not responses.get(BOOTSTRAP_SKIP_BUILD_KEY): errors.extend(self.check_apt_repos(responses)) if errors: log.error("Errors: %s", errors) ModuleHelper.display_failed_check_dialog(self, errors) return False else: self.parent.footer.set_text("No errors found.") return responses def check_apt_repos(self, responses): errors = [] http_proxy = responses[BOOTSTRAP_HTTP_PROXY_KEY].strip() https_proxy = responses[BOOTSTRAP_HTTPS_PROXY_KEY].strip() proxies = { 'http': http_proxy, 'https': https_proxy } repos = responses.get(BOOTSTRAP_REPOS_KEY) if not repos: errors.append("Specify at least one repository.") for index, repo in enumerate(repos): name = repo['name'] priority = repo['priority'] if priority and not isinstance(priority, (types.IntType, types.NoneType)): errors.append("Priority value for repository {0} should be " "empty or numeric.".format(name)) if not name: name = "#{0}".format(index + 1) errors.append("Empty name for repository {0}." .format(name)) if not all((repo['type'], repo['uri'], repo['suite'])): errors.append("Cannot parse repository {0}. " "Expected format: " "'deb uri distribution [component1] [...]'." .format(name)) continue if not self._check_repo(repo['uri'], repo['suite'], proxies): errors.append("URL for repository {0} is not accessible." .format(name)) return errors def apply(self, args): responses = self.check(args) if responses is False: log.error("Check failed. Not applying") log.error("%s" % (responses)) return False self.save(responses) return True def cancel(self, button): ModuleHelper.cancel(self, button) def _get_repo_list_response(self, list_box): # Here we assumed that we get object of WalkerStoredListBox # which contains link for list_walker. if not hasattr(list_box, 'list_walker'): return [] external_lw = list_box.list_walker # on UI we have labels, but not keys... label_to_key_mapping = dict((v['label'], k) for k, v in six.iteritems(self.repo_value_scheme)) result = [] for lb in external_lw: repo = {} internal_lw = getattr(lb, 'list_walker', None) if not internal_lw: continue for edit in internal_lw: if not hasattr(edit, 'caption'): continue key = label_to_key_mapping[edit.caption.strip()] repo[key] = edit.edit_text if any(repo.values()): # skip empty entries result.append(self._parse_ui_repo_entry(repo)) return result def _parse_ui_repo_entry(self, repo_from_ui): regexp = r"(?P\w+) (?P[^\s]+) (?P[^\s]+)( " \ r"(?P
[\w\s]*))?" priority = repo_from_ui.get('priority') name = repo_from_ui.get('name') uri = repo_from_ui.get('uri', '') try: priority = int(priority) if priority else None except (TypeError, ValueError): log.debug("Wrong priority value for repository '{0}': " "'{1}'.".format(name, priority)) match = re.match(regexp, uri) repo_type = match.group('type') if match else '' repo_suite = match.group('suite') if match else '' repo_section = match.group('section') if match else '' repo_uri = match.group('uri') if match else uri return { "name": name, "type": repo_type, "uri": repo_uri, "priority": priority, "suite": repo_suite, "section": repo_section } def _parse_config_repo_entry(self, repo_from_config): uri_template = "{type} {uri} {suite}" section_suffix = " {section}" section = repo_from_config.get('section') name = repo_from_config.get('name', '') priority = repo_from_config.get('priority', '') if not priority: # we can get None from config priority = "" data = { "suite": repo_from_config.get('suite', ''), "type": repo_from_config.get('type', ''), "uri": repo_from_config.get('uri', '') } if section: data["section"] = section uri_template += section_suffix uri = '' if any(data.values()): uri = uri_template.format(**data).strip() result = { "uri": uri, "name": name, "priority": str(priority) } return result def _parse_config_repo_list(self, repos_from_config): # There are two different formats for repositories in config and UI: # on UI we have 3 fields: Name, URI and Priority, # where 'URI' actually contains repo type, suite and section. Example: # deb http://archive.ubuntu.com/ubuntu/ trusty main universe multiverse # In config file 'uri', 'type', 'suite' and 'section' are stored # separate. So we need to convert this list from one format to another. repos_for_ui = [] for entry in repos_from_config: repos_for_ui.append(self._parse_config_repo_entry(entry)) 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( dict((k, "") for k in self.repo_value_scheme)) button_position = self._calculate_field_position(ADD_REPO_BUTTON_KEY) self._redraw_screen(defaults, button_position) def _update_defaults(self, defaults, new_settings): for setting in defaults: try: new_value = ModuleHelper.get_setting(new_settings, setting) if BOOTSTRAP_REPOS_KEY == setting: defaults[setting]["value"] = \ self._parse_config_repo_list(new_value) continue defaults[setting]["value"] = new_value except KeyError: log.warning("no setting named {0} found.".format(setting)) except Exception as e: log.warning("unexpected error: {0}".format(e)) def load(self): settings = self.parent.settings ModuleHelper.load_to_defaults(settings, self.defaults) self._update_defaults(self.defaults, settings) self._select_fields_to_show(self.defaults) def save(self, responses): newsettings = ModuleHelper.make_settings_from_responses(responses) self.parent.settings.merge(newsettings) # Update self.defaults self._update_defaults(self.defaults, self.parent.settings) def check_url(self, url, proxies): try: resp = requests.get(url, proxies=proxies, verify=False) except (requests.exceptions.RequestException, requests.exceptions.BaseHTTPError) as e: log.error(e) return False return resp.ok def _check_repo(self, base_url, suite, proxies): release_url = '{base_url}/dists/{suite}/Release'.format( base_url=base_url, suite=suite) if (local_repo_pattern.search(release_url) and utils.is_pre_deployment()): # Due to pre-deployment stage we can't check accessibility of local # repository since it is not created at that moment. Although we # still should provide an ability to use it, because in fact # it will be created at the end of deployment. log.warning('Accessibility check is skipped for local ' 'repository: {}'.format(release_url)) return True return self.check_url(release_url, proxies) def refresh(self): pass def _generate_screen_by_defaults(self, defaults): screen = ModuleHelper.screenUI(self, self.header_content, self.fields, defaults) return screen def _get_fresh_defaults(self): defaults = copy.copy(self.defaults) self._update_defaults(defaults, ModuleHelper.make_settings_from_responses( self.responses)) return defaults def _redraw_screen(self, defaults=None, focus_position=None): if not defaults: defaults = self.defaults self.screen = self._generate_screen_by_defaults(defaults) if focus_position: self.walker.set_focus(focus_position) self.parent.draw_child_screen(self.screen, focus_on_child=True) def _calculate_field_position(self, field_name): return self._calculate_edits_offset() + self.fields.index(field_name) def _calculate_edits_offset(self): """Returns position of first widget.""" first_edit = self.edits[0] if self.edits else None result = 0 for widget in self.walker.lst: if widget == first_edit: break result += 1 return result def _select_fields_to_show(self, defaults): skip_build = defaults[BOOTSTRAP_SKIP_BUILD_KEY].get('value') if skip_build: self.fields = self._common_fields return self.fields = \ self._common_fields +\ self._repo_related_fields def skip_build_callback(self, widget, new_state): defaults = self._get_fresh_defaults() defaults[BOOTSTRAP_SKIP_BUILD_KEY]['value'] = new_state self._select_fields_to_show(defaults) self._redraw_screen( defaults, self._calculate_field_position(BOOTSTRAP_SKIP_BUILD_KEY)) def screenUI(self): return self._generate_screen_by_defaults(self.defaults)