Files
nodepool/nodepool/cmd/nodepoolcmd.py
James E. Blair 4ef3ebade8 Update references of build "number" to "id"
This follows the previous change and is intended to have little
or no behavior changes (only a few unit tests are updated to use
different placeholder values).  It updates all textual references
of build numbers to build ids to better reflect that they are
UUIDs instead of integers.

Change-Id: I04b5eec732918f5b9b712f8caab2ea4ec90e9a9f
2023-08-02 11:18:15 -07:00

481 lines
18 KiB
Python

# Copyright 2013 OpenStack Foundation
#
# 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 json
import logging.config
import os
from prettytable import PrettyTable
from nodepool import launcher
from nodepool import provider_manager
from nodepool import status
from nodepool.zk import zookeeper as zk
from nodepool.zk import ZooKeeperClient
from nodepool.cmd import NodepoolApp
from nodepool.cmd.config_validator import ConfigValidator
log = logging.getLogger(__name__)
class NodePoolCmd(NodepoolApp):
def create_parser(self):
parser = super(NodePoolCmd, self).create_parser()
parser.add_argument('-c', dest='config',
default='/etc/nodepool/nodepool.yaml',
help='path to config file')
parser.add_argument('-s', dest='secure',
help='path to secure file')
parser.add_argument('--debug', dest='debug', action='store_true',
help='show DEBUG level logging')
subparsers = parser.add_subparsers(title='commands',
description='valid commands',
dest='command',
help='additional help')
cmd_list = subparsers.add_parser('list', help='list nodes')
cmd_list.set_defaults(func=self.list)
cmd_list.add_argument('--detail', action='store_true',
help='Output detailed node info')
cmd_image_list = subparsers.add_parser(
'image-list', help='list images from providers')
cmd_image_list.set_defaults(func=self.image_list)
cmd_dib_image_list = subparsers.add_parser(
'dib-image-list',
help='list images built with diskimage-builder')
cmd_dib_image_list.set_defaults(func=self.dib_image_list)
cmd_image_status = subparsers.add_parser(
'image-status',
help='list image status')
cmd_image_status.set_defaults(func=self.image_status)
cmd_image_build = subparsers.add_parser(
'image-build',
help='build image using diskimage-builder')
cmd_image_build.add_argument('image', help='image name')
cmd_image_build.set_defaults(func=self.image_build)
cmd_alien_image_list = subparsers.add_parser(
'alien-image-list',
help='list images not accounted for by nodepool')
cmd_alien_image_list.set_defaults(func=self.alien_image_list)
cmd_alien_image_list.add_argument('provider', help='provider name',
nargs='?')
cmd_delete = subparsers.add_parser(
'delete',
help='place a node in the DELETE state')
cmd_delete.set_defaults(func=self.delete)
cmd_delete.add_argument('id', help='node id')
cmd_delete.add_argument('--now',
action='store_true',
help='delete the node in the foreground')
cmd_hold = subparsers.add_parser(
'hold',
help='place a node in the HOLD state '
'e.g. for running maintenance tasks')
cmd_hold.set_defaults(func=self.hold)
cmd_hold.add_argument('id', help='node id')
cmd_image_delete = subparsers.add_parser(
'image-delete',
help='delete an image')
cmd_image_delete.set_defaults(func=self.image_delete)
cmd_image_delete.add_argument('--provider', help='provider name',
required=True)
cmd_image_delete.add_argument('--image', help='image name',
required=True)
cmd_image_delete.add_argument('--upload-id', help='image upload id',
required=True)
cmd_image_delete.add_argument('--build-id', help='image build id',
required=True)
cmd_dib_image_delete = subparsers.add_parser(
'dib-image-delete',
help='Delete a dib built image from disk along with all cloud '
'uploads of this image')
cmd_dib_image_delete.set_defaults(func=self.dib_image_delete)
cmd_dib_image_delete.add_argument('id', help='dib image id')
cmd_config_validate = subparsers.add_parser(
'config-validate',
help='Validate configuration file')
cmd_config_validate.set_defaults(func=self.config_validate)
cmd_request_list = subparsers.add_parser(
'request-list',
help='list the current node requests')
cmd_request_list.set_defaults(func=self.request_list)
cmd_info = subparsers.add_parser(
'info',
help='Show provider data from zookeeper')
cmd_info.add_argument(
'provider',
help='Provider name',
metavar='PROVIDER')
cmd_info.set_defaults(func=self.info)
cmd_erase = subparsers.add_parser(
'erase',
help='Erase provider data from zookeeper')
cmd_erase.add_argument(
'provider',
help='Provider name',
metavar='PROVIDER')
cmd_erase.add_argument(
'--force',
help='Bypass the warning prompt',
action='store_true')
cmd_erase.set_defaults(func=self.erase)
cmd_image_pause = subparsers.add_parser(
'image-pause',
help='pause an image')
cmd_image_pause.set_defaults(func=self.image_pause)
cmd_image_pause.add_argument('image', help='image name')
cmd_image_unpause = subparsers.add_parser(
'image-unpause',
help='unpause an image')
cmd_image_unpause.set_defaults(func=self.image_unpause)
cmd_image_unpause.add_argument('image', help='image name')
cmd_export_image_data = subparsers.add_parser(
'export-image-data',
help='Export image data from ZooKeeper')
cmd_export_image_data.add_argument(
'path',
type=str,
help='Export file path')
cmd_export_image_data.set_defaults(func=self.export_image_data)
cmd_import_image_data = subparsers.add_parser(
'import-image-data',
help='Import image data to ZooKeeper')
cmd_import_image_data.add_argument(
'path',
type=str,
help='Import file path')
cmd_import_image_data.set_defaults(func=self.import_image_data)
return parser
def setup_logging(self):
# NOTE(jamielennox): This should just be the same as other apps
if self.args.debug:
m = '%(asctime)s %(levelname)s %(name)s: %(message)s'
logging.basicConfig(level=logging.DEBUG, format=m)
elif self.args.logconfig:
super(NodePoolCmd, self).setup_logging()
else:
m = '%(asctime)s %(levelname)s %(name)s: %(message)s'
logging.basicConfig(level=logging.INFO, format=m)
l = logging.getLogger('kazoo')
l.setLevel(logging.WARNING)
l = logging.getLogger('nodepool.ComponentRegistry')
l.setLevel(logging.WARNING)
def list(self, node_id=None, detail=False):
if hasattr(self.args, 'detail'):
detail = self.args.detail
fields = ['id', 'provider', 'label', 'server_id',
'public_ipv4', 'ipv6', 'state', 'age', 'locked']
if detail:
fields.extend(['pool', 'hostname', 'private_ipv4', 'AZ',
'username', 'connection_port', 'launcher',
'allocated_to', 'hold_job',
'comment', 'user_data', 'driver_data'])
results = status.node_list(self.zk, node_id)
print(status.output(results, 'pretty', fields))
def dib_image_list(self):
results = status.dib_image_list(self.zk)
print(status.output(results, 'pretty'))
def image_status(self):
results = status.image_status(self.zk)
print(status.output(results, 'pretty'))
def image_list(self):
results = status.image_list(self.zk)
print(status.output(results, 'pretty'))
def image_build(self, diskimage=None):
diskimage = diskimage or self.args.image
if diskimage not in self.pool.config.diskimages:
# only can build disk images, not snapshots
raise Exception("Trying to build a non disk-image-builder "
"image: %s" % diskimage)
if self.pool.config.diskimages[diskimage].pause:
raise Exception(
"Skipping build request for image %s; paused" % diskimage)
self.zk.submitBuildRequest(diskimage)
def alien_image_list(self):
self.pool.updateConfig()
t = PrettyTable(["Provider", "Name", "Image ID"])
t.align = 'l'
for provider in self.pool.config.providers.values():
if (self.args.provider and
provider.name != self.args.provider):
continue
manager = self.pool.getProviderManager(provider.name)
# Build list of provider images as known by the provider
provider_images = []
try:
# Only consider images marked as managed by nodepool.
# Prevent cloud-provider images from showing
# up in alien list since we can't do anything about them
# anyway.
provider_images = [
image for image in manager.adapter._listImages()
if 'nodepool_build_id' in image['properties']]
except Exception as e:
log.warning("Exception listing alien images for %s: %s"
% (provider.name, str(e)))
alien_ids = []
uploads = []
for image in provider.diskimages:
# Build list of provider images as recorded in ZK
for bnum in self.zk.getBuildIds(image):
uploads.extend(
self.zk.getUploads(image, bnum,
provider.name,
states=[zk.READY])
)
# Calculate image IDs present in the provider, but not in ZK
provider_image_ids = set([img['id'] for img in provider_images])
zk_image_ids = set([img.external_id for img in uploads])
alien_ids = provider_image_ids - zk_image_ids
for image in provider_images:
if image['id'] in alien_ids:
t.add_row([provider.name, image['name'], image['id']])
print(t)
def delete(self):
node = self.zk.getNode(self.args.id)
if not node:
print("Node id %s not found" % self.args.id)
return
self.zk.lockNode(node, blocking=True, timeout=5)
if self.args.now:
if node.provider not in self.pool.config.providers:
print("Provider %s for node %s not defined on this launcher" %
(node.provider, node.id))
return
provider = self.pool.config.providers[node.provider]
manager = provider_manager.get_provider(provider)
manager.start(self.zk)
node_deleter = manager.startNodeCleanup(node)
node_deleter.join()
manager.stop()
else:
node.state = zk.DELETING
self.zk.storeNode(node)
self.zk.unlockNode(node)
self.list(node_id=node.id)
def hold(self):
node = self.zk.getNode(self.args.id)
if not node:
print("Node id %s not found" % self.args.id)
return
self.zk.lockNode(node, blocking=True, timeout=5)
node.state = zk.HOLD
self.zk.storeNode(node)
self.zk.unlockNode(node)
self.list(node_id=node.id)
def dib_image_delete(self):
(image, build_id) = self.args.id.rsplit('-', 1)
build = self.zk.getBuild(image, build_id)
if not build:
print("Build %s not found" % self.args.id)
return
if build.state == zk.BUILDING:
print("Cannot delete a build in progress")
return
build.state = zk.DELETING
self.zk.storeBuild(image, build, build.id)
def image_delete(self):
provider_name = self.args.provider
image_name = self.args.image
build_id = self.args.build_id
upload_id = self.args.upload_id
image = self.zk.getImageUpload(image_name, build_id, provider_name,
upload_id)
if not image:
print("Image upload not found")
return
if image.state == zk.UPLOADING:
print("Cannot delete because image upload in progress")
return
image.state = zk.DELETING
self.zk.storeImageUpload(image.image_name, image.build_id,
image.provider_name, image, image.id)
def erase(self):
def do_erase(provider_name, provider_builds, provider_nodes):
print("Erasing build data for %s..." % provider_name)
self.zk.removeProviderBuilds(provider_name, provider_builds)
print("Erasing node data for %s..." % provider_name)
self.zk.removeProviderNodes(provider_name, provider_nodes)
provider_name = self.args.provider
provider_builds = self.zk.getProviderBuilds(provider_name)
provider_nodes = self.zk.getProviderNodes(provider_name)
if self.args.force:
do_erase(provider_name, provider_builds, provider_nodes)
else:
print("\nWARNING! This action is not reversible!")
answer = input("Erase ZooKeeper data for provider %s? [N/y] " %
provider_name)
if answer.lower() != 'y':
print("Aborting. No data erased.")
else:
do_erase(provider_name, provider_builds, provider_nodes)
def info(self):
provider_name = self.args.provider
provider_uploads = self.zk.getProviderUploads(provider_name)
provider_nodes = self.zk.getProviderNodes(provider_name)
print("ZooKeeper data for provider %s\n" % provider_name)
print("Image data:")
t = PrettyTable(['Image Name', 'Build ID', 'Upload IDs'])
t.align = 'l'
for image in sorted(provider_uploads):
for build in sorted(provider_uploads[image]):
uploads = provider_uploads[image][build]
upload_ids = sorted([u.id for u in uploads])
t.add_row([image, build, ','.join(upload_ids)])
print(t)
print("\nNodes:")
t = PrettyTable(['ID', 'Server ID'])
t.align = 'l'
for node in provider_nodes:
t.add_row([node.id, node.external_id])
print(t)
def config_validate(self):
validator = ConfigValidator(self.args.config)
return validator.validate()
# TODO(asselin,yolanda): add validation of secure.conf
def request_list(self):
results = status.request_list(self.zk)
print(status.output(results, 'pretty'))
def image_pause(self):
image_name = self.args.image
self.zk.setImagePaused(image_name, True)
def image_unpause(self):
image_name = self.args.image
self.zk.setImagePaused(image_name, False)
def export_image_data(self):
data = self.zk.exportImageData()
with open(os.open(self.args.path,
os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
json.dump(data, f)
def import_image_data(self):
with open(self.args.path, 'r') as f:
self.zk.importImageData(json.load(f))
def _wait_for_threads(self, threads):
for t in threads:
if t:
t.join()
def run(self):
self.zk = None
# no arguments, print help messaging, then exit with error(1)
if not self.args.command:
self.parser.print_help()
return 1
# commands which do not need to start-up or parse config
if self.args.command in ('config-validate'):
return self.args.func()
self.pool = launcher.NodePool(self.args.secure, self.args.config)
config = self.pool.loadConfig()
# commands needing ZooKeeper
if self.args.command in ('image-build', 'dib-image-list',
'image-status',
'image-list', 'dib-image-delete',
'image-delete', 'alien-image-list',
'list', 'delete', 'hold',
'request-list', 'info', 'erase',
'image-pause', 'image-unpause',
'export-image-data', 'import-image-data'):
self.zk_client = ZooKeeperClient(
config.zookeeper_servers,
tls_cert=config.zookeeper_tls_cert,
tls_key=config.zookeeper_tls_key,
tls_ca=config.zookeeper_tls_ca,
timeout=config.zookeeper_timeout,
)
self.zk_client.connect()
self.zk = zk.ZooKeeper(self.zk_client, enable_cache=False)
self.pool.setConfig(config)
self.args.func()
if self.zk:
self.zk.disconnect()
def main():
return NodePoolCmd.main()