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:
parent
1d98edb046
commit
bf518f743d
|
@ -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))
|
|
@ -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))
|
|
@ -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)
|
|
@ -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")
|
|
@ -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"""
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue