Add activate command to fuel-bootstrap-cli

Change-Id: I0d5e56736421332d5a643fb865469898d78cf94f
Implements: blueprint bootstrap-images-support-in-cli
This commit is contained in:
Artur Svechnikov 2015-12-01 16:49:00 +03:00
parent 820abc5171
commit 0fd93b8801
10 changed files with 237 additions and 38 deletions

View File

@ -0,0 +1,49 @@
# -*- 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 ActivateCommand(command.Command):
"""Activate specified bootstrap image."""
def get_parser(self, prog_name):
parser = super(ActivateCommand, self).get_parser(prog_name)
parser.add_argument(
'id',
type=str,
metavar='ID',
help="ID of bootstrap image to be activated."
" 'centos' can be used instead of ID, then Centos"
" bootstrap image will be used by default."
)
parser.add_argument(
'--notify-webui',
help="Notify WebUI with result of command",
action='store_true'
)
return parser
def take_action(self, parsed_args):
# cliff handles errors by itself
image_uuid = bs_image.call_wrapped_method(
'activate',
parsed_args.notify_webui,
image_uuid=parsed_args.id)
self.app.stdout.write("Bootstrap image {0} has been activated.\n"
.format(image_uuid))

View File

@ -118,12 +118,13 @@ class BuildCommand(command.Command):
" listing."
)
parser.add_argument(
'--extra-file',
dest='extra_files',
'--extra-dir',
dest='extra_dirs',
type=str,
metavar='PATH',
help="Directory that will be injected to the image"
" root filesystem. **NOTE** Files/packages will be"
" root filesystem. The option can be given multiple times."
" **NOTE** Files/packages will be"
" injected after installing all packages, but before"
" generating system initramfs - thus it's possible to"
" adjust initramfs.",
@ -163,12 +164,32 @@ class BuildCommand(command.Command):
type=str,
metavar='DIR',
help="Which directory should be used for building image."
" /tmp/ will be used by default.",
default="/tmp/"
" /tmp/ will be used by default."
)
parser.add_argument(
'--activate',
help="Activate bootstrap image after build",
action='store_true'
)
parser.add_argument(
'--notify-webui',
help="Notify WebUI with result of command",
action='store_true'
)
return parser
def take_action(self, parsed_args):
image_uuid, path = bs_image.make_bootstrap(parsed_args)
image_uuid, path = bs_image.call_wrapped_method(
'build',
parsed_args.notify_webui,
data=vars(parsed_args))
self.app.stdout.write("Bootstrap image {0} has been built: {1}\n"
.format(image_uuid, path))
if parsed_args.activate:
bs_image.import_image(path)
bs_image.call_wrapped_method(
'activate',
parsed_args.notify_webui,
image_uuid=image_uuid)
self.app.stdout.write("Bootstrap image {0} has been activated.\n"
.format(image_uuid))

View File

@ -33,7 +33,7 @@ class DeleteCommand(command.Command):
return parser
def take_action(self, parsed_args):
# cliff handles errors by himself
# cliff handles errors by itself
image_uuid = bs_image.delete(parsed_args.id)
self.app.stdout.write("Bootstrap image {0} has been deleted.\n"
.format(image_uuid))

View File

@ -31,10 +31,28 @@ class ImportCommand(command.Command):
metavar='ARCHIVE_FILE',
help="File name of bootstrap image archive"
)
parser.add_argument(
'--activate',
help="Activate bootstrap image after import",
action='store_true'
)
parser.add_argument(
'--notify-webui',
help="Notify WebUI with result of command"
"Works only with --activate",
action='store_true'
)
return parser
def take_action(self, parsed_args):
# Cliff handles errors by himself
# Cliff handles errors by itself
image_uuid = bs_image.import_image(parsed_args.filename)
self.app.stdout.write("Bootstrap image {0} has been imported.\n"
.format(image_uuid))
if parsed_args.activate:
image_uuid = bs_image.call_wrapped_method(
'activate',
parsed_args.notify_webui,
image_uuid=image_uuid)
self.app.stdout.write("Bootstrap image {0} has been activated\n"
.format(image_uuid))

View File

@ -41,3 +41,6 @@ BOOTSTRAP_MODULES = [
IMAGE_DATA = {'/': ROOTFS}
UBUNTU_RELEASE = 'trusty'
ERROR_MSG = "Ubuntu bootstrap image is not available. Please use"\
" fuel-bootstrap manager for fix it."

View File

@ -0,0 +1,34 @@
# -*- 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 fuelclient import client
class MasterNodeSettings(object):
"""Class for working with Fuel master settings"""
class_api_path = "settings/"
def __init__(self):
self.connection = client.APIClient
def update(self, data):
return self.connection.put_request(
self.class_api_path, data)
def get(self):
return self.connection.get_request(
self.class_api_path)

View File

@ -9,7 +9,7 @@ extend_kopts: "biosdevname=0 debug ignore_loglevel log_buf_len=10M print_fatal_s
# injected after installing all packages, but before
# generating system initramfs - thus it's possible to
# adjust initramfs
extra_files:
extra_dirs:
- /usr/share/fuel_bootstrap_cli/files/trusty
# Save generated bootstrap container to
output_dir: /tmp/

View File

@ -25,6 +25,7 @@ from fuel_agent.utils import utils
from fuel_bootstrap import consts
from fuel_bootstrap import errors
from fuel_bootstrap.objects import master_node_settings
from fuel_bootstrap import settings
from fuel_bootstrap.utils import data as data_util
@ -46,17 +47,17 @@ def get_all():
return data
def parse(image_id):
LOG.debug("Trying to parse [%s] image", image_id)
dir_path = full_path(image_id)
def parse(image_uuid):
LOG.debug("Trying to parse [%s] image", image_uuid)
dir_path = full_path(image_uuid)
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))
.format(image_uuid))
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))
.format(image_uuid))
with open(metafile) as f:
try:
@ -64,36 +65,37 @@ def parse(image_id):
except yaml.YAMLError as e:
raise errors.IncorrectImage("Couldn't parse metadata file for"
" image [{0}] due to {1}"
.format(image_id, e))
.format(image_uuid, 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))
.format(data.get('uuid'), image_uuid))
data['status'] = ACTIVE if is_active(data['uuid']) else ''
data.setdefault('label', '')
return data
def delete(image_id):
dir_path = full_path(image_id)
image = parse(image_id)
def delete(image_uuid):
dir_path = full_path(image_uuid)
image = parse(image_uuid)
if image['status'] == ACTIVE:
raise errors.ActiveImageException("Image [{0}] is active and can't be"
" deleted.".format(image_id))
" deleted.".format(image_uuid))
shutil.rmtree(dir_path)
return image_id
return image_uuid
def is_active(image_id):
return full_path(image_id) == os.path.realpath(
def is_active(image_uuid):
return full_path(image_uuid) == os.path.realpath(
CONF.active_bootstrap_symlink)
def full_path(image_id):
if not os.path.isabs(image_id):
return os.path.join(CONF.bootstrap_images_dir, image_id)
return image_id
def full_path(image_uuid):
if not os.path.isabs(image_uuid):
return os.path.join(CONF.bootstrap_images_dir, image_uuid)
return image_uuid
def import_image(arch_path):
@ -109,14 +111,22 @@ def import_image(arch_path):
raise errors.IncorrectImage("Couldn't parse metadata file"
" due to {0}".format(e))
image_id = data['uuid']
dir_path = full_path(image_id)
image_uuid = data['uuid']
dir_path = full_path(image_uuid)
if os.path.exists(dir_path):
raise errors.ImageAlreadyExists("Image [{0}] already exists."
.format(image_id))
.format(image_uuid))
shutil.move(extract_dir, dir_path)
os.chmod(dir_path, 0o755)
for root, dirs, files in os.walk(dir_path):
for d in dirs:
os.chmod(os.path.join(root, d), 0o755)
for f in files:
os.chmod(os.path.join(root, f), 0o755)
return image_uuid
def extract_to_dir(arch_path, extract_path):
@ -124,8 +134,10 @@ def extract_to_dir(arch_path, extract_path):
tarfile.open(arch_path, 'r').extractall(extract_path)
def make_bootstrap(params):
bootdata_builder = data_util.BootstrapDataBuilder(vars(params))
def make_bootstrap(data=None):
if not data:
data = {}
bootdata_builder = data_util.BootstrapDataBuilder(data)
bootdata = bootdata_builder.build()
LOG.info("Try to build image with data:\n%s", yaml.safe_dump(bootdata))
@ -133,9 +145,63 @@ def make_bootstrap(params):
with tempfile.NamedTemporaryFile() as f:
f.write(yaml.safe_dump(bootdata))
f.flush()
utils.execute('fa_mkbootstrap', '--nouse-syslog', '--data_driver',
'bootstrap_build_image', '--nodebug', '-v',
'--image_build_dir', params.image_build_dir,
'--input_data_file', f.name)
opts = ['fa_mkbootstrap', '--nouse-syslog', '--data_driver',
'bootstrap_build_image', '--nodebug', '-v',
'--input_data_file', f.name]
if data.get('image_build_dir'):
opts.extend(['--image_build_dir', data['image_build_dir']])
utils.execute(*opts)
return bootdata['bootstrap']['uuid'], bootdata['output']
def activate(image_uuid=""):
is_centos = image_uuid.lower() == 'centos'
symlink = CONF.active_bootstrap_symlink
if os.path.lexists(symlink):
os.unlink(symlink)
LOG.debug("Symlink %s was deleted", symlink)
if not is_centos:
parse(image_uuid)
dir_path = full_path(image_uuid)
os.symlink(dir_path, symlink)
LOG.debug("Symlink %s to %s directory has been created",
symlink, dir_path)
else:
LOG.warning("WARNING: switching to depracated centos-bootstrap")
# FIXME: Do normal activation when it become clear how to do it
flavor = 'centos' if is_centos else 'ubuntu'
utils.execute('fuel-bootstrap-image-set', flavor)
return image_uuid
def call_wrapped_method(name, notify_webui, **kwargs):
wrapped_methods = {
'build': make_bootstrap,
'activate': activate
}
failed = False
try:
return wrapped_methods[name](**kwargs)
except Exception:
failed = True
raise
finally:
if notify_webui:
notify_webui_about_results(failed, consts.ERROR_MSG)
def notify_webui_about_results(failed, error_message):
mn_settings = master_node_settings.MasterNodeSettings()
settings = mn_settings.get()
settings['settings'].setdefault('bootstrap', {}).setdefault('error', {})
if not failed:
error_message = ""
settings['settings']['bootstrap']['error']['value'] = error_message
mn_settings.update(settings)

View File

@ -52,7 +52,7 @@ class BootstrapDataBuilder(object):
self.root_ssh_authorized_file = \
data.get('root_ssh_authorized_file') or \
CONF.root_ssh_authorized_file
self.extra_files = data.get('extra_files') or CONF.extra_files
self.extra_dirs = data.get('extra_dirs')
self.include_kernel_module = data.get('include_kernel_module')
self.blacklist_kernel_module = data.get('blacklist_kernel_module')
@ -74,7 +74,7 @@ class BootstrapDataBuilder(object):
'extend_kopts': self.extend_kopts,
'post_script_file': self.post_script_file,
'uuid': self.uuid,
'extra_files': self.extra_files,
'extra_files': self._get_extra_dirs(),
'root_ssh_authorized_file': self.root_ssh_authorized_file,
'container': {
'meta_file': consts.METADATA_FILE,
@ -89,6 +89,14 @@ class BootstrapDataBuilder(object):
'image_data': self._prepare_image_data()
}
def _get_extra_dirs(self):
dirs = set()
if self.extra_dirs:
dirs |= set(self.extra_dirs)
if CONF.extra_dirs:
dirs |= set(CONF.extra_dirs)
return list(dirs)
def _prepare_modules(self):
modules = copy.copy(consts.BOOTSTRAP_MODULES)
for module in modules: