diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/__init__.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/delete.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/delete.py new file mode 100644 index 0000000..59742f8 --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/delete.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# 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. + +from cliff import command + +from fuel_bootstrap.utils import bootstrap_image as bs_image + + +class DeleteCommand(command.Command): + """Delete specified bootstrap image from the system.""" + + def get_parser(self, prog_name): + parser = super(DeleteCommand, self).get_parser(prog_name) + parser.add_argument( + 'id', + type=str, + metavar='ID', + help="ID of bootstrap image to be deleted" + ) + return parser + + def take_action(self, parsed_args): + # cliff handles errors by himself + image_uuid = bs_image.delete(parsed_args.id) + self.app.stdout.write("Bootstrap image {0} has been deleted.\n" + .format(image_uuid)) diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/import.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/import.py new file mode 100644 index 0000000..c4b83b8 --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/import.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# 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. + +from cliff import command + +from fuel_bootstrap.utils import bootstrap_image as bs_image + + +class ImportCommand(command.Command): + """Import already created bootstrap image to the system.""" + + def get_parser(self, prog_name): + parser = super(ImportCommand, self).get_parser(prog_name) + # shouldn't we check archive file type? + parser.add_argument( + 'filename', + type=str, + metavar='ARCHIVE_FILE', + help="File name of bootstrap image archive" + ) + return parser + + def take_action(self, parsed_args): + # Cliff handles errors by himself + image_uuid = bs_image.import_image(parsed_args.filename) + self.app.stdout.write("Bootstrap image {0} has been imported" + .format(image_uuid)) diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/list.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/list.py new file mode 100644 index 0000000..6d339ad --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/commands/list.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# 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. + +from cliff import command +from cliff import lister + +from fuelclient.common import data_utils + +from fuel_bootstrap.utils import bootstrap_image as bs_image + + +class ListCommand(lister.Lister, command.Command): + """List all available bootstrap images.""" + + columns = ('uuid', 'label', 'status') + + def get_parser(self, prog_name): + parser = super(ListCommand, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + data = bs_image.get_all() + data = data_utils.get_display_data_multi(self.columns, data) + return (self.columns, data) diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/consts.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/consts.py new file mode 100644 index 0000000..81faf4e --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/consts.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +# 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 os + +# FIXME: make the image directory configurable +BOOTSTRAP_IMAGES_DIR = "/var/www/nailgun/bootstrap" +METADATA_FILE = "metadata.yaml" +SYMLINK = os.path.join(BOOTSTRAP_IMAGES_DIR, "active_bootstrap") diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/errors.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/errors.py new file mode 100644 index 0000000..6be18f6 --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/errors.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# 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. + + +class FuelBootstrapException(Exception): + """Base Exception for Fuel-Bootstrap + + All child classes must be instantiated before raising. + """ + def __init__(self, *args, **kwargs): + super(FuelBootstrapException, self).__init__(*args, **kwargs) + self.message = args[0] + + +class ActiveImageException(FuelBootstrapException): + """Should be raised when action can't be permited to active image""" + + +class ImageAlreadyExists(FuelBootstrapException): + """Should be raised when image with same uuid already exists""" + + +class NotImplemented(FuelBootstrapException): + """Should be raised when some method lacks implementation""" + + +class IncorrectRepository(FuelBootstrapException): + """Should be raised when repository can't be parsed""" + + +class IncorrectImage(FuelBootstrapException): + """Should be raised when image has incorrect format""" diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/main.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/main.py new file mode 100644 index 0000000..83bafdf --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/main.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# 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 logging +import sys + +from cliff import app +from cliff.commandmanager import CommandManager + +LOG = logging.getLogger(__name__) + + +class FuelBootstrap(app.App): + """Main cliff application class. + + Performs initialization of the command manager and + configuration of basic engines. + + """ + + def initialize_app(self, argv): + LOG.debug('initialize app') + + def prepare_to_run_command(self, cmd): + LOG.debug('preparing following command to run: %s', + cmd.__class__.__name__) + + def clean_up(self, cmd, result, err): + LOG.debug('clean up %s', cmd.__class__.__name__) + if err: + LOG.debug('got an error: %s', err) + + +def main(argv=sys.argv[1:]): + fuel_bootstrap_app = FuelBootstrap( + description='Command line Fuel bootstrap manager', + version='0.0.2', + command_manager=CommandManager('fuel_bootstrap', + convert_underscores=True) + ) + return fuel_bootstrap_app.run(argv) diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/utils/__init__.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/utils/bootstrap_image.py b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/utils/bootstrap_image.py new file mode 100644 index 0000000..1476b23 --- /dev/null +++ b/contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap/utils/bootstrap_image.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +# 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 logging +import os +import shutil +import tarfile +import tempfile +import yaml + +from fuel_bootstrap import consts +from fuel_bootstrap import errors + + +LOG = logging.getLogger(__name__) +ACTIVE = 'active' + + +def get_all(): + data = [] + LOG.debug("Searching images in %s", consts.BOOTSTRAP_IMAGES_DIR) + for name in os.listdir(consts.BOOTSTRAP_IMAGES_DIR): + if not os.path.isdir(os.path.join(consts.BOOTSTRAP_IMAGES_DIR, name)): + continue + try: + data.append(parse(name)) + except errors.IncorrectImage as e: + LOG.debug("Image [%s] is skipped due to %s", name, e) + return data + + +def parse(image_id): + LOG.debug("Trying to parse [%s] image", image_id) + dir_path = full_path(image_id) + if os.path.islink(dir_path) or not os.path.isdir(dir_path): + raise errors.IncorrectImage("There are no such image [{0}]." + .format(image_id)) + + metafile = os.path.join(dir_path, consts.METADATA_FILE) + if not os.path.exists(metafile): + raise errors.IncorrectImage("Image [{0}] doen's contain metadata file." + .format(image_id)) + + with open(metafile) as f: + try: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + raise errors.IncorrectImage("Couldn't parse metadata file for" + " image [{0}] due to {1}" + .format(image_id, e)) + if data.get('uuid') != os.path.basename(dir_path): + raise errors.IncorrectImage("UUID from metadata file [{0}] doesn't" + " equal directory name [{1}]" + .format(data.get('uuid'), image_id)) + + data['status'] = ACTIVE if is_active(data['uuid']) else '' + return data + + +def delete(image_id): + dir_path = full_path(image_id) + image = parse(image_id) + if image['status'] == ACTIVE: + raise errors.ActiveImageException("Image [{0}] is active and can't be" + " deleted.".format(image_id)) + + shutil.rmtree(dir_path) + return image_id + + +def is_active(image_id): + return full_path(image_id) == os.path.realpath(consts.SYMLINK) + + +def full_path(image_id): + if not os.path.isabs(image_id): + return os.path.join(consts.BOOTSTRAP_IMAGES_DIR, image_id) + return image_id + + +def import_image(arch_path): + extract_dir = tempfile.mkdtemp() + extract_to_dir(arch_path, extract_dir) + + metafile = os.path.join(extract_dir, consts.METADATA_FILE) + + with open(metafile) as f: + try: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + raise errors.IncorrectImage("Couldn't parse metadata file" + " due to {0}".format(e)) + + image_id = data['uuid'] + dir_path = full_path(image_id) + + if os.path.exists(dir_path): + raise errors.ImageAlreadyExists("Image [{0}] already exists." + .format(image_id)) + + shutil.move(extract_dir, dir_path) + + +def extract_to_dir(arch_path, extract_path): + LOG.info("Try extract %s to %s", arch_path, extract_path) + tarfile.open(arch_path, 'r').extractall(extract_path) diff --git a/tox.ini b/tox.ini index 14f3ea8..bbe3444 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ downloadcache = ~/cache/pip deps = hacking==0.10.2 commands = flake8 {posargs:fuel_agent} + flake8 {posargs:contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap} [testenv:cover] setenv = VIRTUAL_ENV={envdir}