# -*- coding: utf-8 -*- # # Copyright 2018 Google LLC. All Rights Reserved. # # 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. """Library for safe migrations of config files.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import os import shutil from googlecloudsdk.appengine.datastore import datastore_index_xml from googlecloudsdk.appengine.tools import cron_xml_parser from googlecloudsdk.appengine.tools import dispatch_xml_parser from googlecloudsdk.appengine.tools import queue_xml_parser from googlecloudsdk.core import exceptions from googlecloudsdk.core import log from googlecloudsdk.core.console import console_io from googlecloudsdk.core.util import files _CRON_DESC = 'Translates a cron.xml into cron.yaml.' _QUEUE_DESC = 'Translates a queue.xml into queue.yaml.' _DISPATCH_DESC = 'Translates a dispatch.xml into dispatch.yaml.' _INDEX_DESC = 'Translates a datastore-indexes.xml into index.yaml.' class MigrationError(exceptions.Error): pass def _Bakify(file_path): return file_path + '.bak' class MigrationResult(object): """The changes that are about to be applied on a declarative form. Args: new_files: {str, str} a mapping from absolute file path to new contents of the file, or None if the file should be deleted. """ def __init__(self, new_files): self.new_files = new_files def __eq__(self, other): return self.new_files == other.new_files def __ne__(self, other): return not self == other def _Backup(self): for path in self.new_files.keys(): bak_path = _Bakify(path) if not os.path.isfile(path): continue if os.path.exists(bak_path): raise MigrationError( 'Backup file path [{}] already exists.'.format(bak_path)) log.err.Print('Copying [{}] to [{}]'.format(path, bak_path)) shutil.copy2(path, bak_path) def _WriteFiles(self): for path, new_contents in self.new_files.items(): if new_contents is None: log.err.Print('Deleting [{}]'.format(path)) os.remove(path) else: log.err.Print('{} [{}]'.format( 'Overwriting' if os.path.exists(path) else 'Writing', path)) files.WriteFileContents(path, new_contents) def Apply(self): """Backs up first, then deletes, overwrites and writes new files.""" self._Backup() self._WriteFiles() def _MigrateCronXML(src, dst): """Migration script for cron.xml.""" xml_str = files.ReadFileContents(src) yaml_contents = cron_xml_parser.GetCronYaml(None, xml_str) new_files = {src: None, dst: yaml_contents} return MigrationResult(new_files) def _MigrateQueueXML(src, dst): """Migration script for queue.xml.""" xml_str = files.ReadFileContents(src) yaml_contents = queue_xml_parser.GetQueueYaml(None, xml_str) new_files = {src: None, dst: yaml_contents} return MigrationResult(new_files) def _MigrateDispatchXML(src, dst): """Migration script for dispatch.xml.""" xml_str = files.ReadFileContents(src) yaml_contents = dispatch_xml_parser.GetDispatchYaml(None, xml_str) new_files = {src: None, dst: yaml_contents} return MigrationResult(new_files) def _MigrateDatastoreIndexXML(src, dst, auto_src=None): """Migration script for datastore-indexes.xml.""" xml_str = files.ReadFileContents(src) indexes = datastore_index_xml.IndexesXmlToIndexDefinitions(xml_str) new_files = {src: None} if auto_src: xml_str_2 = files.ReadFileContents(auto_src) auto_indexes = datastore_index_xml.IndexesXmlToIndexDefinitions(xml_str_2) indexes.indexes += auto_indexes.indexes new_files[auto_src] = None new_files[dst] = indexes.ToYAML() return MigrationResult(new_files) class MigrationScript(object): """Object representing a migration script and its metadata. Attributes: migrate_fn: a function which accepts a variable number of self-defined kwargs and returns a MigrationResult. description: str, description for help texts and prompts. """ def __init__(self, migrate_fn, description): self.migrate_fn = migrate_fn self.description = description def Run(entry, **kwargs): """Run a migration entry, with prompts and warnings. Args: entry: MigrationScript, the entry to run. **kwargs: keyword args for the migration function. """ result = entry.migrate_fn(**kwargs) # Get errors early log.warning('Please *back up* existing files.\n') console_io.PromptContinue( entry.description, default=True, prompt_string='Do you want to run the migration script now?', cancel_on_no=True) result.Apply() # Registry of all migration entries. Key corresponds to command name REGISTRY = { 'cron-xml-to-yaml': MigrationScript(_MigrateCronXML, _CRON_DESC), 'queue-xml-to-yaml': MigrationScript(_MigrateQueueXML, _QUEUE_DESC), 'dispatch-xml-to-yaml': MigrationScript(_MigrateDispatchXML, _DISPATCH_DESC), 'datastore-indexes-xml-to-yaml': MigrationScript(_MigrateDatastoreIndexXML, _INDEX_DESC), }