def update(self, request, name): """Update a commissioning script.""" script = get_object_or_404(Script, name=name) content = Bin(get_content_parameter(request)) data = request.data.copy() data['script'] = content data['script_type'] = SCRIPT_TYPE.COMMISSIONING form = ScriptForm(instance=script, data=data) if form.is_valid(): form.save(request) return rc.ALL_OK else: return MAASAPIValidationError(form.errors)
def load_builtin_scripts(): for script in BUILTIN_SCRIPTS: if script.inject_file: with open(script.inject_path, 'r') as f: script.substitutes['inject_file'] = f.read() script_content = tempita.Template.from_filename( script.script_path, encoding='utf-8') script_content = script_content.substitute({ 'name': script.name, **script.substitutes, }) try: script_in_db = Script.objects.get(name=script.name) except Script.DoesNotExist: form = ScriptForm(data={ 'script': script_content, 'comment': "Created by maas-%s" % get_maas_version(), }) # Form validation should never fail as these are the scripts which # ship with MAAS. If they ever do this will be cause by unit tests. if not form.is_valid(): raise Exception("%s: %s" % (script.name, form.errors)) script_in_db = form.save(commit=False) script_in_db.default = True script_in_db.save() else: if script_in_db.script.data != script_content: # Don't add back old versions of a script. This prevents two # connected regions with different versions of a script from # fighting with eachother. no_update = False for vtf in script_in_db.script.previous_versions(): if vtf.data == script_content: # Don't update anything if we detect we have an old # version of the builtin scripts no_update = True break if no_update: continue form = ScriptForm(instance=script_in_db, data={ 'script': script_content, 'comment': "Updated by maas-%s" % get_maas_version(), }, edit_default=True) # Form validation should never fail as these are the scripts # which ship with MAAS. If they ever do this will be cause by # unit tests. if not form.is_valid(): raise Exception("%s: %s" % (script.name, form.errors)) script_in_db = form.save(commit=False) script_in_db.default = True script_in_db.save()
def test_packages_validate(self): apt_pkgs = [factory.make_name('apt_pkg') for _ in range(3)] snap_pkg = { 'name': factory.make_name('snap_pkg'), 'channel': random.choice(['stable', 'edge', 'beta', 'candidate']), 'mode': random.choice(['classic', 'dev', 'jail']), } url = factory.make_url() form = ScriptForm( data={ 'script': factory.make_script_content({ 'name': factory.make_name('name'), 'packages': { 'apt': apt_pkgs, 'snap': snap_pkg, 'url': url }, }) }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertDictEqual( { 'apt': apt_pkgs, 'snap': [snap_pkg], 'url': [url] }, script.packages)
def test__update_script_doesnt_effect_other_fields(self): script = factory.make_Script() script_content = factory.make_script_content() name = script.name title = script.title description = script.description tags = script.tags script_type = script.script_type hardware_type = script.hardware_type parallel = script.parallel results = script.results parameters = script.parameters packages = script.packages timeout = script.timeout destructive = script.destructive form = ScriptForm(data={'script': script_content}, instance=script) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(name, script.name) self.assertEquals(title, script.title) self.assertEquals(description, script.description) self.assertEquals(tags, script.tags) self.assertEquals(script_type, script.script_type) self.assertEquals(hardware_type, script.hardware_type) self.assertDictEqual(packages, script.packages) self.assertEquals(parallel, script.parallel) self.assertDictEqual(results, script.results) self.assertDictEqual(parameters, script.parameters) self.assertEquals(timeout, script.timeout) self.assertEquals(destructive, script.destructive) self.assertFalse(script.default) self.assertEquals(script_content, script.script.data)
def test__converts_single_package_to_list(self): apt_pkg = factory.make_name("apt_pkg") snap_pkg = factory.make_name("snap_pkg") url = factory.make_url() form = ScriptForm( data={ "script": factory.make_script_content({ "name": factory.make_name("name"), "packages": { "apt": apt_pkg, "snap": snap_pkg, "url": url, }, }) }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertDictEqual( { "apt": [apt_pkg], "snap": [snap_pkg], "url": [url] }, script.packages, )
def test__create_with_default_values(self): name = factory.make_name("name") script_content = factory.make_script_content() form = ScriptForm(data={"name": name, "script": script_content}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(name, script.name) self.assertEquals("", script.title) self.assertEquals("", script.description) self.assertEquals(1, len(script.tags)) self.assertEquals(SCRIPT_TYPE.TESTING, script.script_type) self.assertEquals(HARDWARE_TYPE.NODE, script.hardware_type) self.assertEquals(SCRIPT_PARALLEL.DISABLED, script.parallel) self.assertDictEqual({}, script.packages) self.assertDictEqual({}, script.results) self.assertDictEqual({}, script.parameters) self.assertEquals(timedelta(0), script.timeout) self.assertFalse(script.destructive) self.assertEquals(script_content, script.script.data) self.assertFalse(script.default) self.assertItemsEqual([], script.for_hardware) self.assertFalse(script.may_reboot) self.assertFalse(script.recommission)
def test_packages_validate(self): apt_pkgs = [factory.make_name("apt_pkg") for _ in range(3)] snap_pkg = { "name": factory.make_name("snap_pkg"), "channel": random.choice(["stable", "edge", "beta", "candidate"]), "mode": random.choice(["classic", "dev", "jail"]), } url = factory.make_url() form = ScriptForm( data={ "script": factory.make_script_content({ "name": factory.make_name("name"), "packages": { "apt": apt_pkgs, "snap": snap_pkg, "url": url, }, }) }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertDictEqual( { "apt": apt_pkgs, "snap": [snap_pkg], "url": [url] }, script.packages, )
def test__loads_yaml_embedded_attributes(self): embedded_yaml = { 'name': factory.make_name('name'), 'title': factory.make_name('title'), 'description': factory.make_name('description'), 'tags': [factory.make_name('tag') for _ in range(3)], 'script_type': factory.pick_choice(SCRIPT_TYPE_CHOICES), 'hardware_type': factory.pick_choice(HARDWARE_TYPE_CHOICES), 'parallel': factory.pick_choice(SCRIPT_PARALLEL_CHOICES), 'results': ['write_speed'], 'parameters': [{'type': 'storage'}, {'type': 'runtime'}], 'packages': {'apt': [factory.make_name('package')]}, 'timeout': random.randint(0, 1000), 'destructive': factory.pick_bool(), } script_content = factory.make_script_content(embedded_yaml) form = ScriptForm(data={'script': script_content}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(embedded_yaml['name'], script.name) self.assertEquals(embedded_yaml['title'], script.title) self.assertEquals(embedded_yaml['description'], script.description) self.assertThat(script.tags, ContainsAll(embedded_yaml['tags'])) self.assertEquals(embedded_yaml['script_type'], script.script_type) self.assertEquals(embedded_yaml['hardware_type'], script.hardware_type) self.assertEquals(embedded_yaml['parallel'], script.parallel) self.assertItemsEqual(embedded_yaml['results'], script.results) self.assertItemsEqual(embedded_yaml['parameters'], script.parameters) self.assertDictEqual(embedded_yaml['packages'], script.packages) self.assertEquals( timedelta(0, embedded_yaml['timeout']), script.timeout) self.assertEquals(embedded_yaml['destructive'], script.destructive) self.assertFalse(script.default) self.assertEquals(script_content, script.script.data)
def test__update_setting_default_has_no_effect(self): script = factory.make_Script(default=True) form = ScriptForm(data={ 'default': False, }, instance=script) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertTrue(script.default)
def test__only_loads_when_script_updated(self): script = factory.make_Script(script=factory.make_script_content( {"name": factory.make_name("name")})) name = factory.make_name("name") form = ScriptForm(instance=script, data={"name": name}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(name, script.name)
def test__allows_list_of_results(self): results = [factory.make_name('result') for _ in range(3)] form = ScriptForm(data={'script': factory.make_script_content({ 'name': factory.make_name('name'), 'results': results, })}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertItemsEqual(results, script.results)
def test__ignores_other_version_yaml(self): script = factory.make_Script() name = script.name form = ScriptForm(instance=script, data={ 'script': factory.make_script_content( {'name': factory.make_name('name')}, version='9.0')}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(name, script.name)
def test__create_setting_default_has_no_effect(self): form = ScriptForm(data={ 'name': factory.make_name('name'), 'script': factory.make_script_content(), 'default': True, }) self.assertTrue(form.is_valid()) script = form.save() self.assertFalse(script.default)
def tests_yaml_tags_can_be_list_of_strings(self): tags = [factory.make_name('tag') for _ in range(3)] form = ScriptForm(data={'script': factory.make_script_content({ 'name': factory.make_name('name'), 'tags': tags, })}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertThat(script.tags, ContainsAll(tags))
def test_create_setting_default_has_no_effect(self): form = ScriptForm( data={ "name": factory.make_name("name"), "script": factory.make_script_content(), "default": True, }) self.assertTrue(form.is_valid()) script = form.save() self.assertFalse(script.default)
def test__type_aliased_to_script_type(self): script_type = factory.pick_choice(SCRIPT_TYPE_CHOICES) form = ScriptForm(data={ 'name': factory.make_name('name'), 'script': factory.make_script_content(), 'type': script_type, }) self.assertTrue(form.is_valid()) script = form.save() self.assertEquals(script_type, script.script_type)
def update(self, request, name): """Update a commissioning script. :param name: The name of the script. :type name: unicode :param title: The title of the script. :type title: unicode :param description: A description of what the script does. :type description: unicode :param tags: A comma seperated list of tags for this script. :type tags: unicode :param type: The type defines when the script should be used. Can be testing or commissioning, defaults to testing. :type script_type: unicode :param timeout: How long the script is allowed to run before failing. 0 gives unlimited time, defaults to 0. :type timeout: unicode :param destructive: Whether or not the script overwrites data on any drive on the running system. Destructive scripts can not be run on deployed systems. Defaults to false. :type destructive: boolean :param script: The content of the script to be uploaded in binary form. note: this is not a normal parameter, but a file upload. Its filename is ignored; MAAS will know it by the name you pass to the request. Optionally you can ignore the name and script parameter in favor of uploading a single file as part of the request. :param comment: A comment about what this change does. :type comment: unicode """ if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) data = request.data.copy() if 'script' in request.FILES: data['script'] = request.FILES.get('script').read() elif len(request.FILES) == 1: for name, script_content in request.FILES.items(): data['name'] = name data['script'] = script_content.read() form = ScriptForm(instance=script, data=data) if form.is_valid(): return form.save() else: raise MAASAPIValidationError(form.errors)
def test_type_aliased_to_script_type(self): script_type = factory.pick_choice(SCRIPT_TYPE_CHOICES) form = ScriptForm( data={ "name": factory.make_name("name"), "script": factory.make_script_content(), "type": script_type, }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(script_type, script.script_type)
def test__can_use_parallel(self): script_parallel = factory.pick_choice(SCRIPT_PARALLEL_CHOICES) form = ScriptForm(data={ 'name': factory.make_name('name'), 'script': factory.make_script_content(), 'parallel': script_parallel, }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(script_parallel, script.parallel)
def test__yaml_doesnt_update_timeout(self): script = factory.make_Script() orig_timeout = script.timeout form = ScriptForm( data={'script': factory.make_script_content({ 'timeout': random.randint(0, 1000), })}, instance=script) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(orig_timeout, script.timeout)
def test__yaml_doesnt_update_tags(self): script = factory.make_Script() orig_tags = script.tags form = ScriptForm( data={'script': factory.make_script_content({ 'tags': [factory.make_name('tag') for _ in range(3)], })}, instance=script) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertItemsEqual(orig_tags, script.tags)
def load_builtin_scripts(): for script in BUILTIN_SCRIPTS: if script.inject_file: with open(script.inject_path, "r") as f: script.substitutes["inject_file"] = f.read() script_content = tempita.Template.from_filename(script.script_path, encoding="utf-8") script_content = script_content.substitute({ "name": script.name, **script.substitutes }) form = None try: script_in_db = Script.objects.get(name=script.name) except Script.DoesNotExist: form = ScriptForm( data={ "script": script_content, "comment": f"Created by maas-{get_maas_version()}", }) else: if script_in_db.script.data != script_content: # Don't add back old versions of a script. This prevents two # connected regions with different versions of a script from # fighting with eachother. for vtf in script_in_db.script.previous_versions(): if vtf.data == script_content: # Don't update anything if we detect we have an old # version of the builtin scripts break else: form = ScriptForm( instance=script_in_db, data={ "script": script_content, "comment": f"Updated by maas-{get_maas_version()}", }, edit_default=True, ) if form is not None: # Form validation should never fail as these are the scripts # which ship with MAAS. If they ever do this will be cause by # unit tests. assert ( form.is_valid() ), f"Builtin script {script.name} caused these errors: {form.errors}" script_in_db = form.save(commit=False) if NODE_INFO_SCRIPTS.get(script.name, {}).get("run_on_controller"): script_in_db.add_tag("deploy-info") else: script_in_db.remove_tag("deploy-info") script_in_db.default = True script_in_db.save()
def test__can_use_hardware_type_name(self): hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES) form = ScriptForm(data={ 'name': factory.make_name('name'), 'script': factory.make_script_content(), 'hardware_type': hardware_type, }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(hardware_type, script.hardware_type)
def test__update(self): script = factory.make_Script() name = factory.make_name('name') title = factory.make_name('title') description = factory.make_name('description') tags = [factory.make_name('tag') for _ in range(3)] script_type = factory.pick_choice(SCRIPT_TYPE_CHOICES) hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES) parallel = factory.pick_choice(SCRIPT_PARALLEL_CHOICES) packages = {'apt': [factory.make_name('package')]} timeout = random.randint(0, 1000) destructive = factory.pick_bool() script_content = factory.make_script_content() comment = factory.make_name('comment') orig_script_content = script.script.data form = ScriptForm(data={ 'name': name, 'title': title, 'description': description, 'tags': ','.join(tags), 'type': script_type, 'hardware_type': hardware_type, 'parallel': parallel, 'packages': json.dumps(packages), 'timeout': str(timeout), 'destructive': destructive, 'script': script_content, 'comment': comment, }, instance=script) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertEquals(name, script.name) self.assertEquals(title, script.title) self.assertEquals(description, script.description) self.assertThat(script.tags, ContainsAll(tags)) self.assertEquals(script_type, script.script_type) self.assertEquals(hardware_type, script.hardware_type) self.assertEquals(parallel, script.parallel) self.assertDictEqual({}, script.results) self.assertDictEqual({}, script.parameters) self.assertDictEqual(packages, script.packages) self.assertEquals(timedelta(0, timeout), script.timeout) self.assertEquals(destructive, script.destructive) self.assertEquals(script_content, script.script.data) self.assertEquals(comment, script.script.comment) self.assertEquals(orig_script_content, script.script.previous_version.data) self.assertEquals(None, script.script.previous_version.comment) self.assertFalse(script.default)
def create(self, request): """Create a new commissioning script. Each commissioning script is identified by a unique name. By convention the name should consist of a two-digit number, a dash, and a brief descriptive identifier consisting only of ASCII characters. You don't need to follow this convention, but not doing so opens you up to risks w.r.t. encoding and ordering. The name must not contain any whitespace, quotes, or apostrophes. A commissioning machine will run each of the scripts in lexicographical order. There are no promises about how non-ASCII characters are sorted, or even how upper-case letters are sorted relative to lower-case letters. So where ordering matters, use unique numbers. Scripts built into MAAS will have names starting with "00-maas" or "99-maas" to ensure that they run first or last, respectively. Usually a commissioning script will be just that, a script. Ideally a script should be ASCII text to avoid any confusion over encoding. But in some cases a commissioning script might consist of a binary tool provided by a hardware vendor. Either way, the script gets passed to the commissioning machine in the exact form in which it was uploaded. :param name: Unique identifying name for the script. Names should follow the pattern of "25-burn-in-hard-disk" (all ASCII, and with numbers greater than zero, and generally no "weird" characters). :param content: A script file, to be uploaded in binary form. Note: this is not a normal parameter, but a file upload. Its filename is ignored; MAAS will know it by the name you pass to the request. """ content = Bin(get_content_parameter(request)) data = request.data.copy() data["script"] = content data["script_type"] = SCRIPT_TYPE.COMMISSIONING form = ScriptForm(data=data) if form.is_valid(): script = form.save(request) return { "name": script.name, "content": b64encode(script.script.data.encode()), "deprecated": ("The commissioning-scripts endpoint is deprecated. " "Please use the node-scripts endpoint."), "resource_uri": reverse("commissioning_script_handler", args=[script.name]), } else: raise MAASAPIValidationError(form.errors)
def test__allows_list_of_results(self): results = [factory.make_name("result") for _ in range(3)] form = ScriptForm( data={ "script": factory.make_script_content({ "name": factory.make_name("name"), "results": results }) }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertItemsEqual(results, script.results)
def tests_yaml_tags_can_be_list_of_strings(self): tags = [factory.make_name("tag") for _ in range(3)] form = ScriptForm( data={ "script": factory.make_script_content({ "name": factory.make_name("name"), "tags": tags }) }) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertThat(script.tags, ContainsAll(tags))
def test__converts_single_package_to_list(self): apt_pkg = factory.make_name('apt_pkg') snap_pkg = factory.make_name('snap_pkg') url = factory.make_url() form = ScriptForm(data={'script': factory.make_script_content({ 'name': factory.make_name('name'), 'packages': {'apt': apt_pkg, 'snap': snap_pkg, 'url': url}, })}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertDictEqual( {'apt': [apt_pkg], 'snap': [snap_pkg], 'url': [url]}, script.packages)
def test__allows_dictionary_of_results(self): results = { factory.make_name('result_key'): { 'title': factory.make_name('result_title') for _ in range(3)} for _ in range(3)} form = ScriptForm(data={'script': factory.make_script_content({ 'name': factory.make_name('name'), 'results': results, })}) self.assertTrue(form.is_valid(), form.errors) script = form.save() self.assertDictEqual(results, script.results)
def test__update_allows_editing_tag_and_timeout_on_default_script(self): script = factory.make_Script(default=True, destructive=False) tags = [factory.make_name('tag') for _ in range(3)] timeout = random.randint(0, 1000) form = ScriptForm(data={ 'tags': ','.join(tags), 'timeout': str(timeout), }, instance=script) self.assertTrue(form.is_valid()) script = form.save() self.assertThat(script.tags, ContainsAll(tags)) self.assertEquals(timedelta(0, timeout), script.timeout)