def __init__(self, config, simulate, hostnames = None, offline = False, deploy = True, remote = None): self._config = config self.offline = offline self.deploy = deploy self._offline_files = None self.remote = remote self._sched = Scheduler() if "agent" in config and "logfile" in config["agent"]: logging.basicConfig(filename=config["agent"]["logfile"], filemode='w', level=logging.DEBUG) if hostnames is None or len(hostnames) == 0: self._hostnames = [self._get_hostname()] else: self._hostnames = hostnames self._config = config self.stop = False self._dm = DependencyManager() self._queue = QueueManager() self._last_update = 0 if not self.offline: self._loader = CodeLoader(self._config["agent"]["code_dir"])
class Agent(object): """ An agent to enact changes upon resources. This agent listens to the message bus for changes. """ def __init__(self, config, simulate, hostnames = None, offline = False, deploy = True, remote = None): self._config = config self.offline = offline self.deploy = deploy self._offline_files = None self.remote = remote self._sched = Scheduler() if "agent" in config and "logfile" in config["agent"]: logging.basicConfig(filename=config["agent"]["logfile"], filemode='w', level=logging.DEBUG) if hostnames is None or len(hostnames) == 0: self._hostnames = [self._get_hostname()] else: self._hostnames = hostnames self._config = config self.stop = False self._dm = DependencyManager() self._queue = QueueManager() self._last_update = 0 if not self.offline: self._loader = CodeLoader(self._config["agent"]["code_dir"]) def _connect(self): """ Connect to the message bus """ # connect self._conn = amqp.Connection(host = self._config["communication"]["host"], userid = self._config["communication"]["user"], password = self._config["communication"]["password"], virtual_host = "/") self._exchange_name = self._config["communication"]["exchange"] self._channel = self._conn.channel() self._channel.exchange_declare(exchange = self._exchange_name, type = "topic") result = self._channel.queue_declare(exclusive = True) queue_name = result[0] self._subscribe_resource(queue_name) self._channel.basic_consume(queue = queue_name, callback=self.on_message, no_ack=True) while self._channel.callbacks: self._channel.wait() def _subscribe_resource(self, queue_name): """ Subscribe to the resource updates on the message bus """ LOGGER.info("Subscribing %s to resource updates" % self._hostnames) for name in self._hostnames: short_hostname = name.split(".")[0] self._channel.queue_bind(exchange = self._exchange_name, queue = queue_name, routing_key = "resources.%s.*" % (short_hostname)) if name != short_hostname: self._channel.queue_bind(exchange = self._exchange_name, queue = queue_name, routing_key = "resources.%s.*" % (name)) self._channel.queue_bind(exchange = self._exchange_name, queue = queue_name, routing_key="control") self._channel.queue_bind(exchange = self._exchange_name, queue = queue_name, routing_key="updated") def on_error(self, headers, message): """ Called when an error is received """ LOGGER.error("Received an error %s" % message) def update(self, res_obj): """ Process an update """ for req in res_obj.requires: self._dm.add_dependency(res_obj, req.version, req.resource_str()) self._queue.add_resource(res_obj) self._last_update = time.time() def get_facts(self, res): """ Get status """ try: provider = Commander.get_provider(self, res.id) except Exception: LOGGER.error("Unable to find a handler for %s" % res.id) return provider.facts(res) def _mq_send(self, routing_key, operation, body): body["operation"] = operation body["source"] = self._hostnames msg = amqp.Message(json.dumps(body)) msg.content_type = "application/json" self._channel.basic_publish(msg, exchange = self._exchange_name, routing_key = routing_key) def _handle_op(self, operation, message): """ Handle an operation """ if operation == "PING": LOGGER.info("Got ping request, sending pong back") response = {"hostname" : self._hostnames } self._mq_send("control", "PONG", response) elif operation == "UPDATE": LOGGER.debug("Received update for %s", message["resource"]["id"]) resource = Resource.deserialize(message["resource"]) self.update(resource) elif operation == "UPDATED": rid = Id.parse_id(message["id"]) version = message["version"] reload = message["reload"] self._dm.resource_update(rid.resource_str(), version, reload) elif operation == "STATUS": resource = Id.parse_id(message["id"]).get_instance() if resource is None: self._mq_send("control", "STATUS_REPLY", {"code" : 404}) return try: provider = Commander.get_provider(self, resource.id) except Exception: LOGGER.exception("Unable to find a handler for %s" % resource) try: result = provider.check_resource(resource) self._mq_send("control", "STATUS_REPLY", result) except Exception: LOGGER.exception("Unable to check status of %s" % resource) self._mq_send("control", "STATUS_REPLY", {"code" : 404}) elif operation == "FACTS": resource_id = Id.parse_id(message["id"]) try: resource = Resource.deserialize(message["resource"]) provider = Commander.get_provider(self, resource_id) try: result = provider.facts(resource) response = {"operation" : "FACTS_REPLY", "subject" : str(resource_id), "facts" : result} self._mq_send("control", "FACTS_REPLY", response) except Exception: LOGGER.exception("Unable to retrieve fact") self._mq_send("control", "FACTS_REPLY", {"subject" : str(resource_id), "code": 404}) except Exception: LOGGER.exception("Unable to find a handler for %s" % resource_id) elif operation == "QUEUE": response = {"queue" : ["%s,v=%d" % (x.id, x.version) for x in self._queue.all()]} self._mq_send("control", "QUEUE_REPLY", response) elif operation == "DEPLOY": self.deploy_config() elif operation == "INFO": response = {"threads" : [x.name for x in enumerate()], "queue length" : self._queue.size(), "queue ready length" : self._queue.ready_size(), } self._mq_send("control", "INFO_REPLY", response) elif operation == "DUMP": LOGGER.info("Dumping!") self._queue.dump() elif operation == "MODULE_UPDATE": version = message["version"] modules = message["modules"] self._loader.deploy_version(version, modules) def on_message(self, msg): """ Method called when a message is received """ body = json.loads(msg.body) # first check if it is for us match = False if "agent" not in body: match = True else: for h in self._hostnames: if fnmatch.fnmatch(h, body["agent"]): match = True if "operation" in body and match: return self._handle_op(body["operation"], body) def _get_hostname(self): """ Determine the hostname of this machine """ return socket.gethostname() def run(self): """ The main loop of the agent. """ self._sched.add_action(self.deploy_config, 10) LOGGER.debug("Scheduled deploy config every 10 seconds") self._sched.add_action(self.ping, 600) LOGGER.debug("Scheduled ping broadcast every 600 seconds") #self._sched.add_action(self.get_desired_state, FORCE_UPDATE_INTERVAL) #LOGGER.debug("Scheduled fetching the desired state from the server synchronously every %d" % FORCE_UPDATE_INTERVAL) self._sched.start() if not self.offline: self._connect() while True: time.sleep(1) def get_desired_state(self): """ Retrieve the desired state from the IMP server if we have not received any updates in the last X seconds """ if (self._last_update + FORCE_UPDATE_INTERVAL) < time.time(): # for an update by fetching an update from the IMP server for name in self._hostnames: conn = client.HTTPConnection("127.0.0.1", 8888) conn.request("GET", "/agentstate/" + name) res = conn.getresponse() data = res.read().decode("utf-8") for res in json.loads(data): self.update(res) def ping(self): """ Broadcast a ping to let everyone know we are here """ LOGGER.info("Sending out a ping") response = {"hostname" : self._hostnames } self._mq_send("control", "PONG", response) def deploy_config(self): """ Deploy a configuration is there are items in the queue """ LOGGER.debug("Execute deploy config") LOGGER.info("Need to update %d resources" % self._queue.size()) while self._queue.size() > 0: resource = self._queue.pop() if resource is None: LOGGER.info("No resources ready for deploy.") break try: provider = Commander.get_provider(self, resource.id) except Exception as e: LOGGER.exception("Unable to find a handler for %s" % resource.id, e) # TODO: submit failure self._queue.remove(resource) continue try: provider.execute(resource, self.deploy) if resource.do_reload and provider.can_reload(): LOGGER.warning("Reloading %s because of updated dependencies" % resource.id) provider.do_reload(resource) LOGGER.debug("Finished %s" % resource) self._queue.remove(resource) except Exception as e: LOGGER.exception(e) # TODO: report back self._queue.remove(resource) return def _server_connection(self): """ A connection to a server """ parts = urllib.parse.urlparse(self._config["config"]["server"]) host, port = parts.netloc.split(":") return client.HTTPConnection(host, port) def get_file(self, hash_id): """ Retrieve a file from the fileserver identified with the given hash """ if self.offline: return self._offline_files[hash_id] else: conn = self._server_connection() conn.request("GET", "/file/" + hash_id) res = conn.getresponse() # upload the file if res.status == 404: return None else: data = res.read() return data def resource_updated(self, resource, reload_requires = False): """ A resource with id $rid calls this method to indicate that it is now at version $version. """ reload = False if hasattr(resource, "reload") and resource.reload and reload_requires: reload = True self._dm.resource_update(resource.id.resource_str(), resource.id.version, reload) if not self.offline: # send out the resource update self._mq_send("control", "UPDATED", {"id" : str(resource.id), "version" : resource.id.version, "reload" : reload}) def close(self): """ Cleanup """ Commander.close()