From eeabfedc54fa691b357e9d0436d3e150f48da912 Mon Sep 17 00:00:00 2001 From: Xinran Date: Fri, 29 Sep 2017 13:54:22 +0800 Subject: [PATCH] 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 --- api-ref/source/v1/parameters.yaml | 7 ++++ .../v1/samples/servers/server-create-req.json | 3 +- api-ref/source/v1/servers.inc | 1 + mogan/api/controllers/v1/schemas/servers.py | 1 + mogan/api/controllers/v1/servers.py | 11 +++++ mogan/api/controllers/v1/utils.py | 40 +++++++++++++++++++ mogan/api/validation/parameter_types.py | 8 ++++ mogan/conf/default.py | 4 ++ mogan/engine/api.py | 12 +++--- mogan/engine/flows/create_server.py | 30 ++++++++------ mogan/engine/manager.py | 7 +++- mogan/engine/metadata.py | 6 +-- mogan/engine/rpcapi.py | 5 ++- mogan/tests/unit/engine/test_engine_api.py | 1 + mogan/tests/unit/engine/test_rpcapi.py | 1 + 15 files changed, 112 insertions(+), 25 deletions(-) diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 95350e88..dea1976b 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -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. diff --git a/api-ref/source/v1/samples/servers/server-create-req.json b/api-ref/source/v1/samples/servers/server-create-req.json index b97a425e..99067e05 100644 --- a/api-ref/source/v1/samples/servers/server-create-req.json +++ b/api-ref/source/v1/samples/servers/server-create-req.json @@ -28,7 +28,8 @@ } ], "user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", - "key_name": "test_key" + "key_name": "test_key", + "adminPass": "Qv7Lsc35H4xM" }, "scheduler_hints": { "group": "group1" diff --git a/api-ref/source/v1/servers.inc b/api-ref/source/v1/servers.inc index 423dd887..ca46edb5 100644 --- a/api-ref/source/v1/servers.inc +++ b/api-ref/source/v1/servers.inc @@ -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 diff --git a/mogan/api/controllers/v1/schemas/servers.py b/mogan/api/controllers/v1/schemas/servers.py index 656beb6a..369c1108 100644 --- a/mogan/api/controllers/v1/schemas/servers.py +++ b/mogan/api/controllers/v1/schemas/servers.py @@ -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': { diff --git a/mogan/api/controllers/v1/servers.py b/mogan/api/controllers/v1/servers.py index c9750c39..0c137412 100644 --- a/mogan/api/controllers/v1/servers.py +++ b/mogan/api/controllers/v1/servers.py @@ -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]) diff --git a/mogan/api/controllers/v1/utils.py b/mogan/api/controllers/v1/utils.py index f79331d3..a3d1dc8c 100644 --- a/mogan/api/controllers/v1/utils.py +++ b/mogan/api/controllers/v1/utils.py @@ -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) diff --git a/mogan/api/validation/parameter_types.py b/mogan/api/validation/parameter_types.py index eace15f5..d08c76cd 100644 --- a/mogan/api/validation/parameter_types.py +++ b/mogan/api/validation/parameter_types.py @@ -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' diff --git a/mogan/conf/default.py b/mogan/conf/default.py index ef3d5ebc..a26a32b3 100644 --- a/mogan/conf/default.py +++ b/mogan/conf/default.py @@ -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 = [ diff --git a/mogan/engine/api.py b/mogan/engine/api.py index 730d710d..9503d05b 100644 --- a/mogan/engine/api.py +++ b/mogan/engine/api.py @@ -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) diff --git a/mogan/engine/flows/create_server.py b/mogan/engine/flows/create_server.py index 0c7265df..84da3960 100644 --- a/mogan/engine/flows/create_server.py +++ b/mogan/engine/flows/create_server.py @@ -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': {} diff --git a/mogan/engine/manager.py b/mogan/engine/manager.py index 590355ca..5d9d03b0 100644 --- a/mogan/engine/manager.py +++ b/mogan/engine/manager.py @@ -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, diff --git a/mogan/engine/metadata.py b/mogan/engine/metadata.py index a5fb6292..ab6378e2 100644 --- a/mogan/engine/metadata.py +++ b/mogan/engine/metadata.py @@ -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: diff --git a/mogan/engine/rpcapi.py b/mogan/engine/rpcapi.py index e9ef9e06..66fcec3a 100644 --- a/mogan/engine/rpcapi.py +++ b/mogan/engine/rpcapi.py @@ -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, diff --git a/mogan/tests/unit/engine/test_engine_api.py b/mogan/tests/unit/engine/test_engine_api.py index c6e7aadc..d27b5f62 100644 --- a/mogan/tests/unit/engine/test_engine_api.py +++ b/mogan/tests/unit/engine/test_engine_api.py @@ -224,6 +224,7 @@ class ComputeAPIUnitTest(base.DbTestCase): None, None, None, + None, min_count, max_count) diff --git a/mogan/tests/unit/engine/test_rpcapi.py b/mogan/tests/unit/engine/test_rpcapi.py index 0e0b9db7..58a2f10b 100644 --- a/mogan/tests/unit/engine/test_rpcapi.py +++ b/mogan/tests/unit/engine/test_rpcapi.py @@ -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,