Add activate command to fuel-bootstrap-cli
Change-Id: I0d5e56736421332d5a643fb865469898d78cf94f Implements: blueprint bootstrap-images-support-in-cli
This commit is contained in:
parent
820abc5171
commit
0fd93b8801
@ -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))
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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."
|
||||
|
@ -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)
|
@ -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/
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user