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)
|
1
tox.ini
1
tox.ini
|
@ -19,6 +19,7 @@ downloadcache = ~/cache/pip
|
||||||
deps = hacking==0.10.2
|
deps = hacking==0.10.2
|
||||||
commands =
|
commands =
|
||||||
flake8 {posargs:fuel_agent}
|
flake8 {posargs:fuel_agent}
|
||||||
|
flake8 {posargs:contrib/mk_bootstrap/fuel_bootstrap/fuel_bootstrap}
|
||||||
|
|
||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
setenv = VIRTUAL_ENV={envdir}
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
|
|
Loading…
Reference in New Issue