diff --git a/NEWS b/NEWS index 8e3ecf7247d14c50f7a9b1e2c811ed478d733ecf..f9b8a72e8d883c33518a48d4bad276dd88e569a9 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,17 @@ News ==== +Version 2.7.0 beta0 +------------------- + +*(unreleased)* + +- ``gnt-instance batch-create`` has been changed to use the bulk create + opcode from Ganeti. This lead to incompatible changes in the format of + the JSON file. It's now not a custom dict anymore but a dict + compatible with the ``OpInstanceCreate`` opcode. + + Version 2.6.0 ------------- diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py index fceb359040b92c466a74cdfcc45be1bf6df03993..9e7dc6128812a9b82dd2b94c3314a2013425137e 100644 --- a/lib/client/gnt_instance.py +++ b/lib/client/gnt_instance.py @@ -69,6 +69,8 @@ _LIST_DEF_FIELDS = [ _MISSING = object() _ENV_OVERRIDE = frozenset(["list"]) +_INST_DATA_VAL = ht.TListOf(ht.TDict) + def _ExpandMultiNames(mode, names, client=None): """Expand the given names using the passed mode. @@ -255,23 +257,8 @@ def AddInstance(opts, args): def BatchCreate(opts, args): """Create instances using a definition file. - This function reads a json file with instances defined - in the form:: - - {"instance-name":{ - "disk_size": [20480], - "template": "drbd", - "backend": { - "memory": 512, - "vcpus": 1 }, - "os": "debootstrap", - "primary_node": "firstnode", - "secondary_node": "secondnode", - "iallocator": "dumb"} - } - - Note that I{primary_node} and I{secondary_node} have precedence over - I{iallocator}. + This function reads a json file with L{opcodes.OpInstanceCreate} + serialisations. @param opts: the command line options selected by the user @type args: list @@ -280,130 +267,53 @@ def BatchCreate(opts, args): @return: the desired exit code """ - _DEFAULT_SPECS = {"disk_size": [20 * 1024], - "backend": {}, - "iallocator": None, - "primary_node": None, - "secondary_node": None, - "nics": None, - "start": True, - "ip_check": True, - "name_check": True, - "hypervisor": None, - "hvparams": {}, - "file_storage_dir": None, - "force_variant": False, - "file_driver": "loop"} - - def _PopulateWithDefaults(spec): - """Returns a new hash combined with default values.""" - mydict = _DEFAULT_SPECS.copy() - mydict.update(spec) - return mydict - - def _Validate(spec): - """Validate the instance specs.""" - # Validate fields required under any circumstances - for required_field in ("os", "template"): - if required_field not in spec: - raise errors.OpPrereqError('Required field "%s" is missing.' % - required_field, errors.ECODE_INVAL) - # Validate special fields - if spec["primary_node"] is not None: - if (spec["template"] in constants.DTS_INT_MIRROR and - spec["secondary_node"] is None): - raise errors.OpPrereqError("Template requires secondary node, but" - " there was no secondary provided.", - errors.ECODE_INVAL) - elif spec["iallocator"] is None: - raise errors.OpPrereqError("You have to provide at least a primary_node" - " or an iallocator.", - errors.ECODE_INVAL) - - if (spec["hvparams"] and - not isinstance(spec["hvparams"], dict)): - raise errors.OpPrereqError("Hypervisor parameters must be a dict.", - errors.ECODE_INVAL) + (json_filename,) = args + cl = GetClient() - json_filename = args[0] try: instance_data = simplejson.loads(utils.ReadFile(json_filename)) except Exception, err: # pylint: disable=W0703 ToStderr("Can't parse the instance definition file: %s" % str(err)) return 1 - if not isinstance(instance_data, dict): - ToStderr("The instance definition file is not in dict format.") + if not _INST_DATA_VAL(instance_data): + ToStderr("The instance definition file is not %s" % _INST_DATA_VAL) return 1 - jex = JobExecutor(opts=opts) + instances = [] + possible_params = set(opcodes.OpInstanceCreate.GetAllSlots()) + for (idx, inst) in enumerate(instance_data): + unknown = set(inst.keys()) - possible_params - # Iterate over the instances and do: - # * Populate the specs with default value - # * Validate the instance specs - i_names = utils.NiceSort(instance_data.keys()) # pylint: disable=E1103 - for name in i_names: - specs = instance_data[name] - specs = _PopulateWithDefaults(specs) - _Validate(specs) + if unknown: + # TODO: Suggest closest match for more user friendly experience + raise errors.OpPrereqError("Unknown fields: %s" % + utils.CommaJoin(unknown), errors.ECODE_INVAL) - hypervisor = specs["hypervisor"] - hvparams = specs["hvparams"] + op = opcodes.OpInstanceCreate(**inst) + op.Validate(False) + instances.append(op) - disks = [] - for elem in specs["disk_size"]: - try: - size = utils.ParseUnit(elem) - except (TypeError, ValueError), err: - raise errors.OpPrereqError("Invalid disk size '%s' for" - " instance %s: %s" % - (elem, name, err), errors.ECODE_INVAL) - disks.append({"size": size}) - - utils.ForceDictType(specs["backend"], constants.BES_PARAMETER_COMPAT) - utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES) - - tmp_nics = [] - for field in constants.INIC_PARAMS: - if field in specs: - if not tmp_nics: - tmp_nics.append({}) - tmp_nics[0][field] = specs[field] - - if specs["nics"] is not None and tmp_nics: - raise errors.OpPrereqError("'nics' list incompatible with using" - " individual nic fields as well", - errors.ECODE_INVAL) - elif specs["nics"] is not None: - tmp_nics = specs["nics"] - elif not tmp_nics: - tmp_nics = [{}] - - op = opcodes.OpInstanceCreate(instance_name=name, - disks=disks, - disk_template=specs["template"], - mode=constants.INSTANCE_CREATE, - os_type=specs["os"], - force_variant=specs["force_variant"], - pnode=specs["primary_node"], - snode=specs["secondary_node"], - nics=tmp_nics, - start=specs["start"], - ip_check=specs["ip_check"], - name_check=specs["name_check"], - wait_for_sync=True, - iallocator=specs["iallocator"], - hypervisor=hypervisor, - hvparams=hvparams, - beparams=specs["backend"], - file_storage_dir=specs["file_storage_dir"], - file_driver=specs["file_driver"]) - - jex.QueueJob(name, op) - # we never want to wait, just show the submitted job IDs - jex.WaitOrShow(False) + op = opcodes.OpInstanceMultiAlloc(iallocator=opts.iallocator, + instances=instances) + result = SubmitOrSend(op, opts, cl=cl) - return 0 + # Keep track of submitted jobs + jex = JobExecutor(cl=cl, opts=opts) + + for (status, job_id) in result[constants.JOB_IDS_KEY]: + jex.AddJobId(None, status, job_id) + + results = jex.GetResults() + bad_cnt = len([row for row in results if not row[0]]) + if bad_cnt == 0: + ToStdout("All instances created successfully.") + rcode = constants.EXIT_SUCCESS + else: + ToStdout("There were %s errors during the creation.", bad_cnt) + rcode = constants.EXIT_FAILURE + + return rcode def ReinstallInstance(opts, args): @@ -1530,7 +1440,8 @@ commands = { "[...] -t disk-type -n node[:secondary-node] -o os-type <name>", "Creates and adds a new instance to the cluster"), "batch-create": ( - BatchCreate, [ArgFile(min=1, max=1)], [DRY_RUN_OPT, PRIORITY_OPT], + BatchCreate, [ArgFile(min=1, max=1)], + [DRY_RUN_OPT, PRIORITY_OPT, IALLOCATOR_OPT, SUBMIT_OPT], "<instances.json>", "Create a bunch of instances based on specs in the file."), "console": (