Add admin_password support

Mogan should support to inject an admin password into a bare metal server.

DocImpact
APIImpact

Change-Id: Id3487b7aea699353aedd49a51d9a5a2e250943b9
Implements: bp admin-password-support
This commit is contained in:
Xinran 2017-09-29 13:54:22 +08:00
parent 3b1dc23cf3
commit eeabfedc54
15 changed files with 112 additions and 25 deletions

View File

@ -155,6 +155,13 @@ addresses:
in: body
required: true
type: object
adminPass:
description: |
The administrative password of the server. If you omit this parameter, the operation
generates a new password.
in: body
required: false
type: string
affinity_zone:
description: |
The affinity zone which the server belongs to.

View File

@ -28,7 +28,8 @@
}
],
"user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==",
"key_name": "test_key"
"key_name": "test_key",
"adminPass": "Qv7Lsc35H4xM"
},
"scheduler_hints": {
"group": "group1"

View File

@ -42,6 +42,7 @@ Request
- metadata: metadata
- user_data: user_data
- personality: personality
- adminPass: adminPass
- key_name: key_name
- partitions: partitions
- scheduler_hints: scheduler_hints

View File

@ -27,6 +27,7 @@ create_server = {
'availability_zone': parameter_types.availability_zone,
'image_uuid': parameter_types.image_id,
'flavor_uuid': parameter_types.flavor_id,
'adminPass': parameter_types.admin_password,
'networks': {
'type': 'array', 'minItems': 1,
'items': {

View File

@ -698,6 +698,8 @@ class ServerController(ServerControllerBase):
partitions = server.get('partitions')
personality = server.pop('personality', None)
password = self._get_server_admin_password(server)
injected_files = []
if personality:
for item in personality:
@ -717,6 +719,7 @@ class ServerController(ServerControllerBase):
requested_networks=requested_networks,
user_data=user_data,
injected_files=injected_files,
admin_password=password,
key_name=key_name,
min_count=min_count,
max_count=max_count,
@ -726,6 +729,14 @@ class ServerController(ServerControllerBase):
pecan.response.location = link.build_url('server', servers[0].uuid)
return Server.convert_with_links(servers[0])
def _get_server_admin_password(self, server):
"""Determine the admin password for a server on creation."""
if 'adminPass' in server:
password = server['adminPass']
else:
password = api_utils.generate_password()
return password
@policy.authorize_wsgi("mogan:server", "update")
@wsme.validate(types.uuid, [ServerPatchType])
@expose.expose(Server, types.uuid, body=[ServerPatchType])

View File

@ -15,8 +15,10 @@
import jsonpatch
from oslo_config import cfg
import random
import wsme
from mogan.common.i18n import _
@ -27,6 +29,12 @@ JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
jsonpatch.JsonPointerException,
KeyError)
# Default symbols to use for passwords. Avoids visually confusing characters.
# ~6 bits per symbol
DEFAULT_PASSWORD_SYMBOLS = ('23456789', # Removed: 0,1
'ABCDEFGHJKLMNPQRSTUVWXYZ', # Removed: I, O
'abcdefghijkmnopqrstuvwxyz') # Removed: l
def validate_limit(limit):
if limit is None:
@ -82,3 +90,35 @@ def show_nics(nics):
ret_nics.append({key: value for key, value in nic.items() if key in
show_keys})
return ret_nics
def generate_password(length=None, symbolgroups=DEFAULT_PASSWORD_SYMBOLS):
"""Generate a random password from the supplied symbol groups.
At least one symbol from each group will be included. Unpredictable
results if length is less than the number of symbol groups.
Believed to be reasonably secure (with a reasonable password length!)
"""
if length is None:
length = CONF.password_length
r = random.SystemRandom()
password = [r.choice(s) for s in symbolgroups]
# If length < len(symbolgroups), the leading characters will only
# be from the first length groups. Try our best to not be predictable
# by shuffling and then truncating.
r.shuffle(password)
password = password[:length]
length -= len(password)
# then fill with random characters from all symbol groups
symbols = ''.join(symbolgroups)
password.extend([r.choice(symbols) for _i in range(length)])
# finally shuffle to ensure first x characters aren't from a
# predictable group
r.shuffle(password)
return ''.join(password)

View File

@ -59,6 +59,14 @@ network_port_id = {
'type': 'string', 'format': 'uuid'
}
admin_password = {
# NOTE: admin_password is the admin password of a server
# instance, and it is not stored into mogan's data base.
# In addition, users set sometimes long/strange string
# as password. It is unnecessary to limit string length
# and string pattern.
'type': 'string',
}
flavor_id = {
'type': 'string', 'format': 'uuid'

View File

@ -27,6 +27,10 @@ api_opts = [
help=_('Return server tracebacks in the API response for any '
'error responses. WARNING: this is insecure '
'and should not be used in a production environment.')),
cfg.IntOpt('password_length',
default=12,
min=0,
help='Length of generated server admin passwords.'),
]
exc_log_opts = [

View File

@ -315,8 +315,8 @@ class API(object):
def _create_server(self, context, flavor, image_uuid,
name, description, availability_zone, metadata,
requested_networks, user_data, injected_files,
key_name, min_count, max_count, partitions,
scheduler_hints):
admin_password, key_name, min_count, max_count,
partitions, scheduler_hints):
"""Verify all the input parameters"""
image = self._get_image(context, image_uuid)
iwdi = self._is_whole_disk_image(context, image)
@ -369,6 +369,7 @@ class API(object):
requested_networks,
user_data,
decoded_files,
admin_password,
key_pair,
partitions,
request_spec,
@ -378,8 +379,9 @@ class API(object):
def create(self, context, flavor, image_uuid,
name=None, description=None, availability_zone=None,
metadata=None, requested_networks=None, user_data=None,
injected_files=None, key_name=None, min_count=None,
max_count=None, partitions=None, scheduler_hints=None):
injected_files=None, admin_password=None, key_name=None,
min_count=None, max_count=None, partitions=None,
scheduler_hints=None):
"""Provision servers
Sending server information to the engine and will handle
@ -396,7 +398,7 @@ class API(object):
image_uuid, name, description,
availability_zone, metadata,
requested_networks, user_data,
injected_files, key_name,
injected_files, admin_password, key_name,
min_count, max_count, partitions,
scheduler_hints)

View File

@ -49,7 +49,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
def __init__(self, engine_rpcapi):
requires = ['filter_properties', 'request_spec', 'server',
'requested_networks', 'user_data', 'injected_files',
'key_pair', 'partitions', 'context']
'admin_password', 'key_pair', 'partitions', 'context']
super(OnFailureRescheduleTask, self).__init__(addons=[ACTION],
requires=requires)
self.engine_rpcapi = engine_rpcapi
@ -68,7 +68,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
def _reschedule(self, context, cause, request_spec, filter_properties,
server, requested_networks, user_data, injected_files,
key_pair, partitions):
admin_password, key_pair, partitions):
"""Actions that happen during the rescheduling attempt occur here."""
create_server = self.engine_rpcapi.schedule_and_create_servers
@ -95,6 +95,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
return create_server(context, [server], requested_networks,
user_data=user_data,
injected_files=injected_files,
admin_password=admin_password,
key_pair=key_pair,
partitions=partitions,
request_spec=request_spec,
@ -218,17 +219,22 @@ class GenerateConfigDriveTask(flow_utils.MoganTask):
"""Generate ConfigDrive value the server."""
def __init__(self):
requires = ['server', 'user_data', 'injected_files', 'key_pair',
'configdrive', 'context']
requires = ['server', 'user_data', 'injected_files', 'admin_password',
'key_pair', 'configdrive', 'context']
super(GenerateConfigDriveTask, self).__init__(addons=[ACTION],
requires=requires)
def _generate_configdrive(self, context, server, user_data=None,
files=None, key_pair=None):
files=None, admin_password=None, key_pair=None):
"""Generate a config drive."""
extra_md = {}
if admin_password:
extra_md['admin_pass'] = admin_password
i_meta = server_metadata.ServerMetadata(
server, content=files, user_data=user_data, key_pair=key_pair)
server, content=files, user_data=user_data, key_pair=key_pair,
extra_md=extra_md)
with tempfile.NamedTemporaryFile() as uncompressed:
with configdrive.ConfigDriveBuilder(server_md=i_meta) as cdb:
cdb.make_drive(uncompressed.name)
@ -243,13 +249,12 @@ class GenerateConfigDriveTask(flow_utils.MoganTask):
compressed.seek(0)
return base64.b64encode(compressed.read())
def execute(self, context, server, user_data, injected_files, key_pair,
configdrive):
def execute(self, context, server, user_data, injected_files,
admin_password, key_pair, configdrive):
try:
configdrive['value'] = self._generate_configdrive(
context, server, user_data=user_data, files=injected_files,
key_pair=key_pair)
admin_password=admin_password, key_pair=key_pair)
except Exception as e:
with excutils.save_and_reraise_exception():
msg = ("Failed to build configdrive: %s" %
@ -282,8 +287,8 @@ class CreateServerTask(flow_utils.MoganTask):
def get_flow(context, manager, server, requested_networks, user_data,
injected_files, key_pair, partitions, request_spec,
filter_properties):
injected_files, admin_password, key_pair, partitions,
request_spec, filter_properties):
"""Constructs and returns the manager entrypoint flow
@ -309,6 +314,7 @@ def get_flow(context, manager, server, requested_networks, user_data,
'requested_networks': requested_networks,
'user_data': user_data,
'injected_files': injected_files,
'admin_password': admin_password,
'key_pair': key_pair,
'partitions': partitions,
'configdrive': {}

View File

@ -303,6 +303,7 @@ class EngineManager(base_manager.BaseEngineManager):
requested_networks,
user_data,
injected_files,
admin_password,
key_pair,
partitions,
request_spec=None,
@ -357,6 +358,7 @@ class EngineManager(base_manager.BaseEngineManager):
requested_networks,
user_data,
injected_files,
admin_password,
key_pair,
partitions,
request_spec,
@ -364,8 +366,8 @@ class EngineManager(base_manager.BaseEngineManager):
@wrap_server_fault
def _create_server(self, context, server, requested_networks,
user_data, injected_files, key_pair, partitions,
request_spec=None, filter_properties=None):
user_data, injected_files, admin_password, key_pair,
partitions, request_spec=None, filter_properties=None):
"""Perform a deployment."""
LOG.debug("Creating server: %s", server)
notifications.notify_about_server_action(
@ -384,6 +386,7 @@ class EngineManager(base_manager.BaseEngineManager):
requested_networks,
user_data,
injected_files,
admin_password,
key_pair,
partitions,
request_spec,

View File

@ -54,8 +54,8 @@ class InvalidMetadataPath(Exception):
class ServerMetadata(object):
"""Server metadata."""
def __init__(self, server, content=None, user_data=None,
key_pair=None, extra_md=None):
def __init__(self, server, content=None, user_data=None, key_pair=None,
extra_md=None):
"""Creation of this object should basically cover all time consuming
collection. Methods after that should not cause time delays due to
network operations or lengthy cpu operations.
@ -66,8 +66,8 @@ class ServerMetadata(object):
if not content:
content = []
self.server = server
self.extra_md = extra_md
self.server = server
self.availability_zone = server.availability_zone
if user_data is not None:

View File

@ -50,8 +50,8 @@ class EngineAPI(object):
serializer=serializer)
def schedule_and_create_servers(self, context, servers, requested_networks,
user_data, injected_files, key_pair,
partitions, request_spec,
user_data, injected_files, admin_password,
key_pair, partitions, request_spec,
filter_properties):
"""Signal to engine service to perform a deployment."""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
@ -59,6 +59,7 @@ class EngineAPI(object):
requested_networks=requested_networks,
user_data=user_data,
injected_files=injected_files,
admin_password=admin_password,
key_pair=key_pair,
partitions=partitions,
request_spec=request_spec,

View File

@ -224,6 +224,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
None,
None,
None,
None,
min_count,
max_count)

View File

@ -109,6 +109,7 @@ class RPCAPITestCase(base.DbTestCase):
requested_networks=[],
user_data=None,
injected_files=None,
admin_password=None,
key_pair=None,
partitions=None,
request_spec=None,