def is_registered(self, request): """Returns whether or not the given MAC address is registered within this MAAS (and attached to a non-retired node). :param mac_address: The mac address to be checked. :type mac_address: unicode :return: 'true' or 'false'. :rtype: unicode Returns 400 if any mandatory parameters are missing. """ mac_address = get_mandatory_param(request.GET, 'mac_address') mac_addresses = MACAddress.objects.filter(mac_address=mac_address) mac_addresses = mac_addresses.exclude(node__status=NODE_STATUS.RETIRED) return mac_addresses.exists()
def add_tag(self, request, name): """@description-title Add a tag @description Add a single tag to a script with the given name. @param (string) "{name}" [required=true] The name of the script. @param (string) "tag" [required=false] The tag being added. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing information about the updated script. @success-example "success-json" [exkey=scripts-add-tag] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested script is not found. @error-example "not-found" Not Found """ tag = get_mandatory_param(request.data, 'tag', String) if ',' in tag: raise MAASAPIValidationError('Tag may not contain a ",".') if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) script.add_tag(tag) script.save() create_audit_event(EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=("Added tag '%s' to script '%s'." % (tag, script.name))) return script
def get_config(self, request): """@description-title Get a configuration value @description Get a configuration value. @param (string) "name" [required=true,formatting=true] The name of the configuration item to be retrieved. %s @success (http-status-code) "server-success" 200 @success (content) "default_distro_series" A plain-text string containing the requested value, e.g. ``default_distro_series``. @success-example "default_distro_series" "bionic" """ name = get_mandatory_param(request.GET, "name") name = rewrite_config_name(name) if name in migrated_config_values: value = migrated_config_values[name].getter() else: validate_config_name(name) value = Config.objects.get_config(name) return HttpResponse(json.dumps(value), content_type="application/json")
def remove_tag(self, request, system_id, id): """@description-title Remove a tag @description Remove a tag from block device on a given machine. @param (string) "{system_id}" [required=true] The machine system_id. @param (string) "{id}" [required=true] The block device's id. @param (string) "tag" [required=false] The tag being removed. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing the updated block device. @success-example "success-json" [exkey=block-devs-remove-tag] placeholder text @error (http-status-code) "403" 403 @error (content) "no-perms" The user does not have permissions to remove a tag. @error (http-status-code) "404" 404 @error (content) "not-found" The requested machine or block device is not found. @error-example "not-found" Not Found @error (http-status-code) "409" 409 @error (content) "not-ready" The requested machine is not ready. """ device = BlockDevice.objects.get_block_device_or_404( system_id, id, request.user, NodePermission.admin) node = device.get_node() if node.status != NODE_STATUS.READY: raise NodeStateViolation( "Cannot update block device because the machine is not Ready.") device.remove_tag(get_mandatory_param(request.POST, "tag")) device.save() return device
def remove_tag(self, request, name): """@description-title Remove a tag @description Remove a tag from a script with the given name. @param (string) "{name}" [required=true] The name of the script. @param (string) "tag" [required=false] The tag being removed. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing information about the updated script. @success-example "success-json" [exkey=scripts-remove-tag] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested script is not found. @error-example "not-found" Not Found """ tag = get_mandatory_param(request.data, "tag", String) if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) script.remove_tag(tag) script.save() create_audit_event( EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=("Removed tag '%s' from script '%s'." % (tag, script.name)), ) return script
def remove_tag(self, request, system_id, id): """@description-title Remove a tag from an interface @description Remove a tag from an interface with the given system_id and interface id. @param (string) "{system_id}" [required=true] A system_id. @param (int) "{id}" [required=true] An interface id. @param (string) "tag" [required=false] The tag to remove. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing the updated interface object. @success-example "success-json" [exkey=interfaces-remove-tag] placeholder text @error (http-status-code) "403" 403 @error (content) "403" If the user does not have the permission to add a tag. @error (http-status-code) "404" 404 @error (content) "not-found" The requested machine or interface is not found. @error-example "not-found" Not Found """ interface = Interface.objects.get_interface_or_404( system_id, id, request.user, NodePermission.admin, NodePermission.edit, ) interface.remove_tag(get_mandatory_param(request.POST, "tag")) interface.save() return interface
def remove_tag(self, request, name): """Remove a single tag to a script. :param tag: The tag being removed. :type tag: unicode Returns 404 if the script is not found. """ tag = get_mandatory_param(request.data, 'tag', String) if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) script.remove_tag(tag) script.save() create_audit_event(EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=("Removed tag '%s' from script '%s'." % (tag, script.name))) return script
def delete(self, request, **kwargs): """@description-title Delete a file @description Delete a stored file. @param (string) "filename" [required=true] The filename of the object to be deleted. @success (http-status-code) "server-success" 204 @error (http-status-code) "404" 404 @error (content) "not-found" The requested file is not found. @error-example "not-found" Not Found """ # It is valid for a path in a POST, PUT, or DELETE (or any other HTTP # method) to contain a query string. However, Django only makes # parameters from the query string available in the badly named # request.GET object. filename = get_mandatory_param(request.GET, "filename") stored_file = get_object_or_404(FileStorage, filename=filename, owner=request.user) stored_file.delete() return rc.DELETED
def revert(self, request, name): """Revert a script to an earlier version. :param to: What revision in the script's history to revert to. This can either be an ID or a negative number representing how far back to go. :type to: integer Returns 404 if the script is not found. """ revert_to = get_mandatory_param(request.data, 'to', Int) if name.isdigit(): script = get_object_or_404(Script, id=int(name)) else: script = get_object_or_404(Script, name=name) try: if script.default: raise MAASAPIValidationError("Unable to revert default script") def gc_hook(value): script.script = value script.save() script.script.revert(revert_to, gc_hook=gc_hook) create_audit_event( EVENT_TYPES.SETTINGS, ENDPOINT.API, request, None, description=("Script %s" % script.name + " reverted to revision %s" % revert_to + " for '%(username)s'.")) return script except ValueError as e: raise MAASAPIValidationError(e.args[0])
def signal(self, request, version=None, mac=None): """Signal ephemeral environment status. A node booted into an ephemeral environment can call this to report progress of any scripts given to it by MAAS. Calling this from a node in an invalid state is an error. Signaling completion more than once is not an error; all but the first successful call are ignored. :param status: An ephemeral environment signal. This may be "OK" to signal completion, WORKING for progress reports, FAILED for, FAILURE, COMMISSIONING for requesting the node be put into COMMISSIONING when NEW, TESTING for requesting the node be put into TESTING from COMMISSIONING, TIMEDOUT if the script has run past its time limit, or INSTALLING for installing required packages. :param error: An optional error string. If given, this will be stored (overwriting any previous error string), and displayed in the MAAS UI. If not given, any previous error string will be cleared. :param script_result_id: What ScriptResult this signal is for. If the signal contains a file upload the id will be used to find the ScriptResult row. If the status is "WORKING" the ScriptResult status will be set to running. :param exit_status: The return code of the script run. """ node = get_queried_node(request, for_mac=mac) status = get_mandatory_param(request.POST, 'status', String) target_status = None if (node.status not in self.signalable_states and node.node_type == NODE_TYPE.MACHINE): raise NodeStateViolation( "Machine status isn't valid (status is %s)" % NODE_STATUS_CHOICES_DICT[node.status]) # These statuses are acceptable for commissioning, disk erasing, # entering rescue mode and deploying. if status not in [choice[0] for choice in SIGNAL_STATUS_CHOICES]: raise MAASAPIBadRequest("Unknown status: '%s'" % status) if (node.status not in self.effective_signalable_states and node.node_type == NODE_TYPE.MACHINE): # If commissioning, it is already registered. Nothing to be done. # If it is installing, should be in deploying state. return rc.ALL_OK if node.node_type == NODE_TYPE.MACHINE: process_status_dict = { NODE_STATUS.NEW: self._process_new, NODE_STATUS.TESTING: self._process_testing, NODE_STATUS.COMMISSIONING: self._process_commissioning, NODE_STATUS.DEPLOYING: self._process_deploying, NODE_STATUS.DISK_ERASING: self._process_disk_erasing, NODE_STATUS.ENTERING_RESCUE_MODE: self._process_entering_rescue_mode, NODE_STATUS.RESCUE_MODE: self._process_rescue_mode, } process = process_status_dict[node.status] else: # Non-machine nodes can send testing results when in testing # state, otherwise accept all signals as commissioning signals # regardless of the node's state. This is because devices and # controllers which were not deployed by MAAS will be in a NEW # or other unknown state but may send commissioning data. if node.status == NODE_STATUS.TESTING: process = self._process_testing else: process = self._process_commissioning target_status = process(node, request, status) if target_status in (None, node.status): # No status change. Nothing to be done. return rc.ALL_OK # Only machines can change their status. This is to allow controllers # to send refresh data without having their status changed to READY. # The exception to this is if testing was run. if (node.node_type == NODE_TYPE.MACHINE or node.status == NODE_STATUS.TESTING): node.status = target_status node.error = get_optional_param(request.POST, 'error', '', String) # Done. node.save() return rc.ALL_OK
def get_content_parameter(request): """Get the "content" parameter from a CommissioningScript POST or PUT.""" content_file = get_mandatory_param(request.FILES, "content") return content_file.read()
def release(self, request): """Release an IP address that was previously reserved by the user. :param ip: The IP address to release. :type ip: unicode :param force: If True, allows a MAAS administrator to force an IP address to be released, even if it is not a user-reserved IP address or does not belong to the requesting user. Use with caution. :type force: bool :param discovered: If True, allows a MAAS administrator to release a discovered address. Only valid if 'force' is specified. If not specified, MAAS will attempt to release any type of address except for discovered addresses. Returns 404 if the provided IP address is not found. """ ip = get_mandatory_param(request.POST, "ip") force = get_optional_param(request.POST, 'force', default=False, validator=StringBool) discovered = get_optional_param(request.POST, 'discovered', default=False, validator=StringBool) if force is True and not request.user.is_superuser: return HttpResponseForbidden( content_type='text/plain', content="Force-releasing an IP address requires admin " "privileges.") form = ReleaseIPForm(request.POST) if not form.is_valid(): raise MAASAPIValidationError(form.errors) if force: query = StaticIPAddress.objects.filter(ip=ip) if discovered: query = query.filter(alloc_type=IPADDRESS_TYPE.DISCOVERED) else: query = query.exclude(alloc_type=IPADDRESS_TYPE.DISCOVERED) else: query = StaticIPAddress.objects.filter( ip=ip, alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=request.user) # Get the reserved IP address, or raise bad request. ip_address = query.first() if ip_address is None: if force: error = "IP address %s does not exist." else: error = ( "IP address %s does not exist, is not the correct type of " "address, or does not belong to the requesting user.\n" "If you are sure you want to release this address, use " "force=true as a MAAS administrator.") raise MAASAPIBadRequest(error % ip) from None # Unlink the IP address from the interfaces it is connected. interfaces = list(ip_address.interface_set.all()) if len(interfaces) > 0: for interface in interfaces: interface.unlink_ip_address(ip_address) else: ip_address.delete() # Delete any interfaces that no longer have any IP addresses and have # no associated nodes. for interface in interfaces: if interface.node is None and interface.only_has_link_up(): interface.delete() maaslog.info("User %s%s released IP address: %s (%s).", request.user.username, " forcibly" if force else "", ip, ip_address.alloc_type_name) return rc.DELETED
def create(self, request): """@description-title Create a MAAS user account @description Creates a MAAS user account. This is not safe: the password is sent in plaintext. Avoid it for production, unless you are confident that you can prevent eavesdroppers from observing the request. @param (string) "username" [required=true] Identifier-style username for the new user. @param (string) "email" [required=true] Email address for the new user. @param (string) "password" [required=true] Password for the new user. @param (boolean) "is_superuser" [required=true] Whether the new user is to be an administrator. ('0' == False, '1' == True) @success (http-status-code) "200" 200 @success (json) "success-json" A JSON object containing information about the new user. @success-example "success-json" [exkey=create] placeholder text @error (http-status-code) "400" 400 @error (content) "error-content" Mandatory parameters are missing. @error-example "error-content" No provided username! """ username = get_mandatory_param(request.data, "username") email = get_mandatory_param(request.data, "email") if request.external_auth_info: password = request.data.get("password") else: password = get_mandatory_param(request.data, "password") is_superuser = extract_bool( get_mandatory_param(request.data, "is_superuser") ) create_audit_event( EVENT_TYPES.AUTHORISATION, ENDPOINT.API, request, None, description=( "Created %s '%s'." % ("admin" if is_superuser else "user", username) ), ) if is_superuser: user = User.objects.create_superuser( username=username, password=password, email=email ) if request.data.get("key") is not None: request.user = user sshkeys_handler = SSHKeysHandler() sshkeys_handler.create(request) return user else: return User.objects.create_user( username=username, password=password, email=email )