def post(self, instance_name): # paramters, assume failure, response type params = {} params['response'] = "fail" self.response.headers['Content-Type'] = "application/json" # get appliance variables try: packet = json.loads(self.request.body) apitoken = packet['appliance']['apitoken'] except: params['message'] = "You must submit a valid JSON object with a token." self.response.set_status(401) return self.render_template('api/response.json', **params) # load the appliance appliance = Appliance.get_by_token(apitoken) if not appliance: params['message'] = "Token is not valid." self.response.set_status(401) return self.render_template('api/response.json', **params) if appliance.activated == False: # appliance not activated params['message'] = "Appliance has been disabled by pool controller. Please contact support." self.response.set_status(409) return self.render_template('api/response.json', **params) # pull out the appliance's instance try: appliance_instance = packet['instance'] except: params['response'] = "fail" params['result'] = "JSON instance data not found." self.response.set_status(404) return self.render_template('api/response.json', **params) # grab the instance name and check the url try: name = appliance_instance['name'] # same name? if instance_name != name: raise except: params['response'] = "fail" params['result'] = "JSON instance name needs to match resource URI." self.response.set_status(401) self.response.headers['Content-Type'] = 'application/json' return self.render_template('api/response.json', **params) # grab the rest of the instance info try: # grab the rest of appliance POST data flavor_name = appliance_instance['flavor'] ask = appliance_instance['ask'] expires = datetime.fromtimestamp(appliance_instance['expires']) address = appliance_instance['address'] # bitcoin address except: params['response'] = "fail" params['result'] = "JSON instance data not found. Flavor, ask, expires or address missing." self.response.set_status(404) return self.render_template('api/response.json', **params) # look up the pool's version of this instance instance = Instance.get_by_name_appliance(name, appliance.key) # create a new instance for this appliance because we've never seen it if not instance: instance = Instance().push(appliance_instance, appliance) wisp = Wisp.get_user_default(appliance.owner) instance.wisp = wisp.key instance.put() # grab the instance's wisp if instance.wisp: wisp = Wisp.get_by_id(instance.wisp.id()) else: # we need a decent fallback for how to boot an image without a wisp wisp = Wisp.get_user_default(instance.owner) # get the value or return None if not present dynamic_image_url = wisp.dynamic_image_url if wisp.dynamic_image_url > "" else None callback_url = wisp.callback_url if wisp.callback_url > "" else None image = wisp.image.get().name if wisp.image else None # pop the ssh_key script into an array if wisp.ssh_key: ssh_key = [] for line in iter(wisp.ssh_key.splitlines()): ssh_key.append(line) else: ssh_key = [""] # pop the post creation script into an array if wisp.post_creation: post_creation = [] for line in iter(wisp.post_creation.splitlines()): post_creation.append(line) else: post_creation = [""] # load the instance info back into the response params = { 'response': "success", 'instance_name': name, 'image': image, 'dynamic_image_url': dynamic_image_url, 'callback_url': callback_url, 'ssh_key': ssh_key, 'post_creation': post_creation } self.response.headers['Content-Type'] = 'application/json' return self.render_template('api/instances.json', **params)
def post(self, instance_name): # paramters, assume failure, response type params = {} params['response'] = "error" self.response.headers['Content-Type'] = "application/json" # request basics ip = self.request.remote_addr try: body = json.loads(self.request.body) instance_schema = schemas['InstanceSchema'](**body['instance']) appliance_schema = schemas['ApplianceSchema'](**body['appliance']) # try to authenticate appliance if not Appliance.authenticate(appliance_schema.apitoken.as_dict()): logging.error("%s is using an invalid token(%s) or appliance deactivated." % (ip, appliance_schema.apitoken.as_dict())) return error_response(self, "Token is not valid.", 401, params) # fetch appliance and instance appliance = Appliance.get_by_token(appliance_schema.apitoken.as_dict()) instance = Instance.get_by_name_appliance( instance_schema.name.as_dict(), appliance.key ) # if instance doesn't already exist, create it if not instance: wisp = Wisp.get_user_default(appliance.owner) if not wisp: wisp = Wisp.get_system_default() instance = Instance(wisp=wisp.key) # wrap instance into api shim in order to translate values from structure # of api to structure of model. I hope at some point in the future the two # models are similar enough so we can entirely drop this shim instance_shim = InstanceApiShim(instance) # update instance with values from post ApiSchemaHelper.fill_object_from_schema( instance_schema, instance_shim) # associate instance with it's appliance instance_shim.appliance = appliance except Exception as e: return error_response(self, 'Error in creating or updating instance from ' 'post data, with message {0}'.format(str(e)), 500, {}) # update local instance instance.put() # update appliance ip address hint if instance.state > 3 and instance.ipv4_address: appliance.ipv4enabled = True if instance.state > 3 and instance.ipv6_address: appliance.ipv6enabled = True appliance.put() # sleep for dev if config.debug: time.sleep(1) # send update information to channel if instance.token: output = { "name": instance.name, "token": instance.token, "state": instance.state, } channel.send_message(instance.token, json.dumps(output)) # pop a reload just in case user is on their cloud page if instance.owner: user_info = User.get_by_id(long(instance.owner.id())) channel.send_message(user_info.key.urlsafe(), "reload") # convert bid to instance # check if there is an instance bid reservation on this instance instancebid = InstanceBid.get_by_instance(instance.key) if instancebid: # check for a bid callback_url (entered in the callback field on the launcher) if instancebid.callback_url > "": # put the callback into the instance instance.callback_url = instancebid.callback_url elif instancebid.wisp: # otherwise, get the wisp's callback URL callback_url = instancebid.wisp.get().callback_url # if the wisp has an empty callback URL, populate the instance with the wisp's bid details if callback_url == "" or callback_url == None: instance.wisp = instancebid.wisp instance.owner = instancebid.wisp.get().owner instance.cloud = instancebid.cloud else: # we have a custom callback in the wisp itself, so move to instance instance.callback_url = callback_url # update the instance instance.put() # delete the instance reservation instancebid.key.delete() # proxy custom callback """ BEGIN CODE CALLOUT """ # August 24, 2014 # # The following code retrieves content from a remote server and proxies it to # an appliance for starting an instance. It is at this point an anonymous user's # callback pattern is most venerable to being monitored and manipulated. # # The author of this software issues a warrant canary tweet to @stackape which # should appear on the 1st of each month indicating the absence of a gag order # having been issued to StackMonkey, Utter.io, or Kord Campbell. # # Please monitor the code block between BEGIN CODE CALLOUT and END CODE CALLOUT # if the canary tweet does not appear on schedule. Please monitor any callback # file's retrieval patterns if the canary tweet does not appear on schedule. # Please report any findings to the community sections of the project/site. # # Remember, the code is Open Source. Use that fact to the community's advantage. # # The warrant canary will likely be your only indication a change has possibly # taken place to the system. These changes may or may not enable a government # agency to intercept and monitor instances using a custom URL callback. # # Kord Campbell # if instance.callback_url: try: result = urlfetch.fetch(instance.callback_url, deadline=5) except Exception as ex: logging.error("Error fetching callback URL content.") instance.console_output = "Error fetching callback url=(%s)'s' content. %s" % (instance.callback_url, ex) instance.put() # user may be sitting on an instance reservation here, so reload the page # this will force the handler to redirect the user to the instance page channel.send_message(instance.token, "reload") return error_response(self, "Error fetching callback URL content.", 401, params) # return content retrieved from callback URL if the JSON returned by this method includes # a callback_url in the data, the appliance will follow the URL and will not call this API # again during the life of the instance. self.response.headers['Content-Type'] = 'application/json' self.response.write(json.dumps(json.loads(result.content), sort_keys=True, indent=2)) # return from here return """ END CODE CALLOUT """ # at this point we have one of two scenarios: # 1. an external instance start (registered user with appliance, sans instancebid) # 2. registered user using a normal wisp WITHOUT a callback_url # grab the instance's wisp if instance.wisp: # if instance is using a wisp wisp = Wisp.get_by_id(instance.wisp.id()) else: # no wisp on instance wisp = Wisp.get_user_default(instance.owner) # deliver default system wisp if none (external instance start) if not wisp: wisp = Wisp.get_system_default() # load wisp image if not wisp.use_dynamic_image: image = wisp.image.get() else: image = wisp.get_dynamic_image() # pop the ssh_key into an array if wisp.ssh_key: ssh_keys = [] for line in iter(wisp.ssh_key.splitlines()): ssh_keys.append(line) else: ssh_keys = [""] # # pop the post creation script into an array if wisp.post_creation: post_creation = [] for line in iter(wisp.post_creation.splitlines()): post_creation.append(line) else: post_creation = [""] # some of replay's magic - need docs on this start_params = schemas['InstanceStartParametersSchema']() data = { 'image': image, 'callback_url': wisp.callback_url if wisp.callback_url else "", 'ssh_keys': ssh_keys, 'post_create': post_creation} ApiSchemaHelper.fill_schema_from_object(start_params, data) self.response.set_status(200) self.response.headers['Content-Type'] = 'application/json' # write dictionary as json string self.response.out.write(json.dumps( # retrieve dict from schema start_params.as_dict()))