Add commands to CLI fuel-bootstrap

* fuel-bootstrap list
* fuel-bootstrap import
* fuel-bootstrap delete

Change-Id: I25b4c68b2b2599da179ccb78d4f5eb91e3988ce8
Implements: blueprint bootstrap-images-support-in-cli
This commit is contained in:
Artur Svechnikov 2015-11-05 17:39:21 +03:00
parent 1d98edb046
commit bf518f743d
10 changed files with 357 additions and 0 deletions

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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")

View File

@ -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"""

View File

@ -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)

View File

@ -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)

View File

@ -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}