Our server logical structure is based on the concept of "cluster" and "instance". A cluster is a set of instances of Weblogic and/or Karaf and/or Apache and/or Nginx and/or Jboss. The instances "MAY or MAY-NOT" be spread across multiple hosts.
A classic cluster would be:
hosts: a, b
Instances on a: tst13-apache, tst13-weblogic-ecom, tst13-weblogic-fulfill
Instances on b: tst13-karaf-rest-bus, tst13-karf-rest-svcs, tst13-nginx-cache
I have created a super-class module (myMod) and 5 subclasses (myApache, myNginx, myJboss, myKaraf, myWeblogic). The "instance" information (inventory variables) are kept in an ldap database. The instances are not "registered" in rc.d directories on the hosts. Each server has "generic scripts" for starting an apache instance and a weblogic instance and a JBoss instance and ... by "instance name" so calling Ansible modules such as "httpd" was not practical.
My module will be used for "stop, start, status, kill, etc" (actions) taken against a list of instances.
Ideally, I would like to create an ssh connection to the destination "host" for EACH instance of software that is to be actioned on the destination host (which I believe would mean an instance of a module per instance of server-software that needs an action taken); however, thus far, it seems ansible makes 1 ssh connection per destination "host".
Since a "host" can be running many instances of different types of software (2 apache instances, 4 weblogic instances, and 3 karaf instances could all be on 1 host); it seemed practical to create my "myMod" as follows:
#####
## <ANSIBLE_HOME>/modules/controller.py
#####
import socket
from ansible.module_utils.ldapData import ldapData
from ansible.module_utils.myMod import myMod
from ansible.module_utils.myModules import myApache, myNginx, myWeblogic, myKaraf, myJboss
def main():
arg_spec = dict(
action = dict(default='status', choices=['start', 'stop', 'kill', 'status', 'warm', 'startsession', 'stopsession']),
instances = dict(default='#', required=False, type='str'),
)
host = socket.gethostname()
serverdata = ldapData()
cluster = serverdata.hosts[host]["cluster"]
clusterData = serverdata.clusters[cluster]
hostData = serverdata.hosts[host]
classList = list(myMod.__subclasses__())
for classType in classList:
localMod = classType(arg_spec, hostData, clusterData)
localMod.process()
if __name__ == '__main__':
main()
The process method in myMod looks like this:
#####
## <ANSIBLE_HOME>/module_utils/myMod.py
#####
def process(self):
fPointers = {
'start': self.start,
'stop': self.stop,
'kill': self.kill,
'status': self.status,
'stopsession': self.stopsession,
'startsesson': self.startsession,
'warm': self.warm
}
act = self.params['action']
for inst in self.instances:
if fPointers[act]:
fPointers[act](inst)
else:
undefined(inst)
self.exit_json(changed=self.changed, msg=self.out, warnings=self.warn, errors=self.err)
Currently, I am only getting back the "out" and "warn" and "err" of the first module that gets "process" called. Other modules are either not executed or not reported. I am expecting that exit_json is "exiting the entire Ansible run" and not just "the current module".
Am I correct? Is there a concept at play that I am not aware of? What is the best change here to ensure that the ansible "report" at the end of the run shows "all output of all instances"?
For every task Ansible packages the module with all required libs and parameters before sending it to execution via SSH. So your main module (wrapper) should done all the job in a single run.
exit_json method stops the whole process with sys.exit(0), so when you call this method the first time your module's run is finished.
I'd recommend you to make one AnsibleModule class and a pack of helpers that return data to your parent module. Then you just combine all helpers' results into single object and flush it to stdout (with exit_json).
Related
How to monitor and read Ubuntu's "Night Light" status via D-Bus using Python with dasbus? I can't figure out the API docs on how to read a property or subscribe to a signal.
Likely candidates:
dasbus.client.property.get()
GLibClient.subscribe()
The following is adapted from the basic examples and prints the interfaces and properties/signals of the object:
#!/usr/bin/env python3
from dasbus.connection import SessionMessageBus
bus = SessionMessageBus()
# dasbus.client.proxy.ObjectProxy
proxy = bus.get_proxy(
"org.gnome.SettingsDaemon.Color", # bus name
"/org/gnome/SettingsDaemon/Color", # object path
)
print(proxy.Introspect())
# read and print properties "NightLightActive" and "Temperature" from interface "org.gnome.SettingsDaemon.Color" in (callback) function
# subscribe to signal "PropertiesChanged" in interface "org.freedesktop.DBus.Properties" / register callback function
Resources
https://pypi.org/project/dbus-python/
What is recommended to use pydbus or dbus-python and what are the differences?
https://wiki.python.org/moin/DbusExamples
Migration from dbus to GDbus in Python 3
Looking at the dasbus examples and the Introspection data it looks like to get the property the dasbus is pythonic so proxy.<property name> works. For your example of NightLightActive it would be:
print("Night light active?", proxy.NightLightActive)
For the signal you need to connect to the signal on the proxy so that seems to take the form of proxy.<signal name>.connect so for example:
proxy.PropertiesChanged.connect(callback)
And this will need to have an EventLoop running.
My entire test was:
from dasbus.connection import SessionMessageBus
from dasbus.loop import EventLoop
bus = SessionMessageBus()
loop = EventLoop()
# dasbus.client.proxy.ObjectProxy
proxy = bus.get_proxy(
"org.gnome.SettingsDaemon.Color", # bus name
"/org/gnome/SettingsDaemon/Color", # object path
)
print("Night light active?", proxy.NightLightActive)
print("Temperature is set to:", proxy.Temperature)
def callback(iface, prop_changed, prop_invalidated):
print("The notification:",
iface, prop_changed, prop_invalidated)
proxy.PropertiesChanged.connect(callback)
loop.run()
I am using Python's logging to log execution of functions and other actions within an application. The log files are stored in a remote folder, which is accessible automatically when I connect to VPN (let's say \remote\directory). That is normal situation, 99% of the time there is a connection and log is stored without errors.
I need a solution for a situation when either the VPN connection or Internet connection is lost and the logs are temporarily stored locally. I think that on each time something is attempted to be logged, I need to run a check if the remote folder is accessible. I couldn't really find a solution, but I guess I need to modify the FileHandler somehow.
TLDR: You can already scroll down to blues' answer and my UPDATE section - there is my latest attempt to solve the issue.
Currently my handler is set like this:
log = logging.getLogger('general')
handler_error = logging.handlers.RotatingFileHandler(log_path+"\\error.log", 'a', encoding="utf-8")
log.addHandler(handler_error)
Here is a condition that sets the log path but only once - when logging is initialized. If I think correctly, I would like to run this condition each time the
if (os.path.isdir(f"\\\\remote\\folder\\")): # if remote is accessible
log_path = f"\\\\remote\\folder\\dev\\{d.year}\\{month}\\"
os.makedirs(os.path.dirname(log_path), exist_ok=True) # create this month dir if it does not exist, logging does not handle that
else: # if remote is not accesssible
log_path = f"localFiles\\logs\\dev\\{d.year}\\{month}\\"
log.debug("Cannot access the remote directory. Are you connected to the internet and the VPN?")
I have found a related thread, but was not able to adjust it to my own needs: Dynamic filepath & filename for FileHandler in logger config file in python
Should I dig deeper into custom Handler or is there some other way? Would be enough if I could call my own function that changed the logging path if needed (or change logger to one with a proper path) when logging is being executed.
UPDATE:
Per blues's answer, I have tried modifying a handler to suit my needs. Unfortunately, the code below, in which I try to switch baseFilename between local and remote paths, does not work. The logger always saves the log to local log file (that has been set while initializing logger). Thus, I think that my attempts to modify the baseFilename do not work?
class HandlerCheckBefore(RotatingFileHandler):
print("handler starts")
def emit(self, record):
calltime = date.today()
if os.path.isdir(f"\\\\remote\\Path\\"): # if remote is accessible
print("handler remote")
# create remote folders if not yet existent
os.makedirs(os.path.dirname(f"\\\\remote\\Path\\{calltime.year}\\{calltime.strftime('%m')}\\"), exist_ok=True)
if (self.level >= 20): # if error or above
self.baseFilename = f"\\\\remote\\Path\\{calltime.year}\\{calltime.strftime('%m')}\\error.log"
else:
self.baseFilename = f"\\\\remote\\Path\\{calltime.year}\\{calltime.strftime('%m')}\\{calltime.strftime('%d')}-{calltime.strftime('%m')}.log"
super().emit(record)
else: # save to local
print("handler local")
if (self.level >= 20): # error or above
self.baseFilename = f"localFiles\\logs\\{calltime.year}\\{calltime.strftime('%m')}\\error.log"
else:
self.baseFilename = f"localFiles\\logs\\{calltime.year}\\{calltime.strftime('%m')}\\{calltime.strftime('%d')}-{calltime.strftime('%m')}.log"
super().emit(record)
# init the logger
handler_error = HandlerCheckBefore(f"\\\\remote\\Path\\{calltime.year}\\{calltime.strftime('%m')}\\error.log", 'a', encoding="utf-8")
handler_error.setLevel(logging.ERROR)
handler_error.setFormatter(fmt)
log.addHandler(handler_error)
The best way to solve this is indeed to create a custom Handler for this. You can either check before each write that the directory is still there, or you could attempt to write the log and handle the resulting error in handleError which all loggers call when an exception occurs during emit(). I recommend the former. The code below shows how both could be implemented:
import os
import logging
from logging.handlers import RotatingFileHandler
class GrzegorzRotatingFileHandlerCheckBefore(RotatingFileHandler):
def emit(self, record):
if os.path.isdir(os.path.dirname(self.baseFilename)): # put appropriate check here
super().emit(record)
else:
logging.getLogger('offline').error('Cannot access the remote directory. Are you connected to the internet and the VPN?')
class GrzegorzRotatingFileHandlerHandleError(RotatingFileHandler):
def handleError(self, record):
logging.getLogger('offline').error('Something went wrong when writing log. Probably remote dir is not accessible')
super().handleError(record)
log = logging.getLogger('general')
log.addHandler(GrzegorzRotatingFileHandlerCheckBefore('check.log'))
log.addHandler(GrzegorzRotatingFileHandlerHandleError('handle.log'))
offline_logger = logging.getLogger('offline')
offline_logger.addHandler(logging.FileHandler('offline.log'))
log.error('test logging')
I have a question about how to mock a nested method and test what it was called with. I'm having a hard time getting my head around: https://docs.python.org/3/library/unittest.mock-examples.html#mocking-chained-calls.
I'd like to test that the "put" method from the fabric library is called by the deploy_file method in this class, and maybe what values are given to it. This is the module that gathers some information from AWS and provides a method to take action on the data.
import json
import os
from aws.secrets_manager import get_secret
from fabric import Connection
class Deploy:
def __init__(self):
self.secrets = None
self.set_secrets()
def set_secrets(self):
secrets = get_secret()
self.secrets = json.loads(secrets)
def deploy_file(self, source_file):
with Connection(host=os.environ.get('SSH_USERNAME'), user=os.environ.get("SSH_USERNAME")) as conn:
destination_path = self.secrets["app_path"] + '/' + os.path.basename(source_file)
conn.put(source_file, destination_path)
"get_secret" is a method in another module that uses the boto3 library to get the info from AWS.
These are the tests I'm working on:
from unittest.mock import patch
from fabric import Connection
from jobs.deploy import Deploy
def test_set_secrets_dict_from_expected_json_string():
with patch('jobs.deploy.get_secret') as m_get_secret:
m_get_secret.return_value = '{"app_path": "/var/www/html"}'
deployment = Deploy()
assert deployment.secrets['app_path'] == "/var/www/html"
def test_copy_app_file_calls_fabric_put():
with patch('jobs.deploy.get_secret') as m_get_secret:
m_get_secret.return_value = '{"app_path": "/var/www/html"}'
deployment = Deploy()
with patch('jobs.deploy.Connection', spec=Connection) as m_conn:
local_file_path = "/tmp/foo"
deployment.deploy_file(local_file_path)
m_conn.put.assert_called_once()
where the second test results in "AssertionError: Expected 'put' to have been called once. Called 0 times."
the first test mocks the "get_secret" function just fine to test that the constructor for "Deploy" sets "Deploy.secrets" from the fake AWS data.
In the second test, get_secrets is mocked just as before, and I mock "Connection" from the fabric library. If I don't mock Connection, I get an error related to the "host" parameter when the Connection object is created.
I think that when "conn.put" is called its creating a whole new Mock object and I'm not testing that object when the unittest runs. I'm just not sure how to define the test to actually test the call to put.
I'm also a novice at understanding what to test (and how) and what not to test as well as how to use mock and such. I'm fully bought in on the idea though. It's been very helpful to find bugs and regressions as I work on projects.
I want to change the env.hosts dynamically because sometimes I want to deploy to one machine first, check if ok then deploy to many machines.
Currently I need to set env.hosts first, how could I set the env.hosts in a method and not in global at script start?
Yes you can set env.hosts dynamically. One common pattern we use is:
from fabric.api import env
def staging():
env.hosts = ['XXX.XXX.XXX.XXX', ]
def production():
env.hosts = ['YYY.YYY.YYY.YYY', 'ZZZ.ZZZ.ZZZ.ZZZ', ]
def deploy():
# Do something...
You would use this to chain the tasks such as fab staging deploy or fab production deploy.
Kind of late to the party, but I achieved this with ec2 like so (note in EC2 you do not know what the ip/hostname may be, generally speaking - so you almost have to go dynamic to really account for how the environment/systems could come up - another option would be to use dyndns, but then this would still be useful):
from fabric.api import *
import datetime
import time
import urllib2
import ConfigParser
from platform_util import *
config = ConfigParser.RawConfigParser()
#task
def load_config(configfile=None):
'''
***REQUIRED*** Pass in the configuration to use - usage load_config:</path/to/config.cfg>
'''
if configfile != None:
# Load up our config file
config.read(configfile)
# Key/secret needed for aws interaction with boto
# (anyone help figure out a better way to do this with sub modules, please don't say classes :-) )
global aws_key
global aws_sec
aws_key = config.get("main","aws_key")
aws_sec = config.get("main","aws_sec")
# Stuff for fabric
env.user = config.get("main","fabric_ssh_user")
env.key_filename = config.get("main","fabric_ssh_key_filename")
env.parallel = config.get("main","fabric_default_parallel")
# Load our role definitions for fabric
for i in config.sections():
if i != "main":
hostlist = []
if config.get(i,"use-regex") == 'yes':
for x in get_running_instances_by_regex(aws_key,aws_sec,config.get(i,"security-group"),config.get(i,"pattern")):
hostlist.append(x.private_ip_address)
env.roledefs[i] = hostlist
else:
for x in get_running_instances(aws_key,aws_sec,config.get(i,"security-group")):
hostlist.append(x.private_ip_address)
env.roledefs[i] = hostlist
if config.has_option(i,"base-group"):
if config.get(i,"base-group") == 'yes':
print "%s is a base group" % i
print env.roledefs[i]
# env["basegroups"][i] = True
where get_running_instances and get_running_instances_by_regex are utility functions that make use of boto (http://code.google.com/p/boto/)
ex:
import logging
import re
from boto.ec2.connection import EC2Connection
from boto.ec2.securitygroup import SecurityGroup
from boto.ec2.instance import Instance
from boto.s3.key import Key
########################################
# B-O-F get_instances
########################################
def get_instances(access_key=None, secret_key=None, security_group=None):
'''
Get all instances. Only within a security group if specified., doesnt' matter their state (running/stopped/etc)
'''
logging.debug('get_instances()')
conn = EC2Connection(aws_access_key_id=access_key, aws_secret_access_key=secret_key)
if security_group:
sg = SecurityGroup(connection=conn, name=security_group)
instances = sg.instances()
return instances
else:
instances = conn.get_all_instances()
return instances
Here is a sample of what my config looked like:
# Config file for fabric toolset
#
# This specific configuration is for <whatever> related hosts
#
#
[main]
aws_key = <key>
aws_sec = <secret>
fabric_ssh_user = <your_user>
fabric_ssh_key_filename = /path/to/your/.ssh/<whatever>.pem
fabric_default_parallel = 1
#
# Groupings - Fabric knows them as roledefs (check env dict)
#
# Production groupings
[app-prod]
security-group = app-prod
use-regex = no
pattern =
[db-prod]
security-group = db-prod
use-regex = no
pattern =
[db-prod-masters]
security-group = db-prod
use-regex = yes
pattern = mysql-[d-s]01
Yet another new answer to an old question. :) But I just recently found myself attempting to dynamically set hosts, and really have to disagree with the main answer. My idea of dynamic, or at least what I was attempting to do, was take an instance DNS-name that was just created by boto, and access that instance with a fab command. I couldn't do fab staging deploy, because the instance doesn't exist at fabfile-editing time.
Fortunately, fabric does support a truly dynamic host-assignment with execute. (It's possible this didn't exist when the question was first asked, of course, but now it does). Execute allows you to define both a function to be called, and the env.hosts it should use for that command. For example:
def create_EC2_box(data=fab_base_data):
conn = boto.ec2.connect_to_region(region)
reservations = conn.run_instances(image_id=image_id, ...)
...
return instance.public_dns_name
def _ping_box():
run('uname -a')
run('tail /var/log/cloud-init-output.log')
def build_box():
box_name = create_EC2_box(fab_base_data)
new_hosts = [box_name]
# new_hosts = ['ec2-54-152-152-123.compute-1.amazonaws.com'] # testing
execute(_ping_box, hosts=new_hosts)
Now I can do fab build_box, and it will fire one boto call that creates an instance, and another fabric call that runs on the new instance - without having to define the instance-name at edit-time.
I have a single CherryPy application serving two websites, each having their static files stored in respective sub-folders of my app folder (each subfolder is named after the respective domain). In my main top-level program (Main.py), the site is launched with
cherrypy.quickstart(Root(), '/',config='cherrypy.cfg'). So far so good...
The problem I am having is with static declarations in config.cfg, which usually starts with
[/]
tools.staticdir.root = '/domain name/root/static/folder'
tools.staticdir.on = True
tools.staticdir.dir = ''
[/css]
tools.staticdir.on = True
tools.staticdir.dir = 'css'
However, at the time the app. is launched, I don't know the value of the tools.staticdir.root folder until I get a request, then I can evaulate the domain name (via. cherrypy.request.base) then set the default subfolder path and root folder accordingly.
So the question is, can I 'hold-off' declaring my static files/folders until my Index() method is called (if so, how?), or can they only be declared when cherrypy.quickstart() is run?
TIA,
Alan
All Tools are just callables with some configuration sugar, so you can hold off until your index method via:
def index(self, ...):
root = my_domain_map[cherrypy.request.headers['Host']]
cherrypy.lib.staticdir(section='', dir='', root=root)
# And then this funky hack...
return cherrypy.response.body
index.exposed = True
...or just by calling cherrypy.lib.static.serve_file, which is even lower level...
...but there's a more integrated way. Set the root argument before you get to the index method, and indeed before the staticdir Tool is invoked. It is invoked in a before_handler hook (priority 50; lower numbers run first). So, you want to inspect your Host header somewhere just before that; let's pick priority 30:
def staticroot(debug=False):
root = my_domain_map[cherrypy.request.headers['Host']]
cherrypy.request.toolmaps['tools']['staticdir']['root'] = root
cherrypy.tools.staticroot = cherrypy.Tool(
staticroot, point='before_handler', priority=30)
Then, turn on your new tool in config:
[/]
tools.staticroot.on = True
...and give it a whirl.