Skip to content

Commit e663065

Browse files
author
Nathaniel Felsen
committed
Adding feature to figure the reverse dependecies of roles which is super useful when making a change
1 parent 1ab612f commit e663065

File tree

6 files changed

+174
-10
lines changed

6 files changed

+174
-10
lines changed

ansigenome/constants.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
PACKAGE_RESOURCE = pkg_resources.resource_filename(__name__, "data")
1111

1212
VALID_ACTIONS = ("config", "scan", "gendoc", "genmeta",
13-
"export", "init", "run")
13+
"export", "init", "run", "reverse_dependencies")
1414

1515
ALLOWED_GENDOC_FORMATS = ("rst", "md")
1616
ALLOWED_GRAPH_FORMATS = ("png", "dot")
@@ -174,6 +174,11 @@
174174
" and more",
175175
"help_init": "init new roles with a custom meta file and tests",
176176
"help_run": "run shell commands inside of each role's directory",
177+
"help_reverse_dependencies": "generate a list of reverse dependencies" +
178+
" for a given list of role",
179+
"no_role_found": "No role named '%role' was found in the path",
180+
"role_missing_out": "You must supply -r <role(s)> to get the list of" +
181+
" reverse dependencies",
177182
}
178183

179184
TEST_PATH = os.path.join(os.path.sep, "tmp", "ansigenome")

ansigenome/scan.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import ui as ui
77
import utils as utils
88

9+
import networkx as nx
10+
911
from ansigenome.export import Export
1012

1113

@@ -14,14 +16,16 @@ class Scan(object):
1416
Loop over each role on the roles path and report back stats on them.
1517
"""
1618
def __init__(self, args, options, config,
17-
gendoc=False, genmeta=False, export=False):
19+
gendoc=False, genmeta=False, export=False,
20+
reverse_dependencies=False):
1821
self.roles_path = args[0]
1922

2023
self.options = options
2124
self.config = config
2225
self.gendoc = gendoc
2326
self.genmeta = genmeta
2427
self.export = export
28+
self.reverse_dependencies = reverse_dependencies
2529

2630
# set the readme output format
2731
self.readme_format = c.ALLOWED_GENDOC_FORMATS[0]
@@ -42,6 +46,9 @@ def __init__(self, args, options, config,
4246
self.all_files = []
4347
self.yaml_files = []
4448

49+
if self.reverse_dependencies:
50+
self.gr = nx.DiGraph()
51+
4552
# only load and validate the readme when generating docs
4653
if self.gendoc:
4754
# only change the format if it is different than the default
@@ -85,6 +92,9 @@ def __init__(self, args, options, config,
8592
if self.export:
8693
self.export_roles()
8794

95+
if self.reverse_dependencies:
96+
self.search_in_digraph(options)
97+
8898
def limit_roles(self):
8999
"""
90100
Limit the roles being scanned.
@@ -100,6 +110,42 @@ def limit_roles(self):
100110

101111
self.roles = new_roles
102112

113+
def create_digraph(self):
114+
"""
115+
Create reverse dependencie graph
116+
"""
117+
roles = self.report["roles"]
118+
for role in roles:
119+
dependencies = roles[role]['dependencies']
120+
self.gr.add_node(role)
121+
for dependencie in dependencies:
122+
self.gr.add_node(dependencie)
123+
self.gr.add_edge(dependencie, role)
124+
125+
def search_in_digraph(self, options):
126+
if not options.roles:
127+
ui.error(c.MESSAGES["role_missing_out"])
128+
sys.exit(1)
129+
130+
roles = options.roles
131+
search_result = []
132+
for role in roles.split(","):
133+
try:
134+
dependencies = list(nx.bfs_tree(self.gr, role))
135+
search_result = list(set(search_result) | set(dependencies))
136+
except nx.networkx.exception.NetworkXError:
137+
ui.warn("",
138+
c.MESSAGES["no_role_found"].replace("%role", role),
139+
"")
140+
pass
141+
except:
142+
print "Unexpected error:", sys.exc_info()[0]
143+
if options.out_file:
144+
utils.string_to_file(options.out_file,
145+
"['" + "', '".join(search_result) + "']")
146+
else:
147+
print search_result
148+
103149
def scan_roles(self):
104150
"""
105151
Iterate over each role and report its stats.
@@ -129,13 +175,18 @@ def scan_roles(self):
129175
if self.valid_meta(key):
130176
self.make_meta_dict_consistent()
131177
self.write_meta(key)
132-
else:
178+
elif not self.reverse_dependencies:
133179
self.update_scan_report(key)
134180

135-
if not self.config["options_quiet"] and not self.export:
136-
ui.role(key,
137-
self.report["roles"][key],
138-
self.report["stats"]["longest_role_name_length"])
181+
if not self.config["options_quiet"] and not self.export and \
182+
not self.reverse_dependencies:
183+
ui.role(
184+
key,
185+
self.report["roles"][key],
186+
self.report["stats"]["longest_role_name_length"]
187+
)
188+
if self.reverse_dependencies:
189+
return self.create_digraph()
139190

140191
self.tally_role_columns()
141192

ansigenome/test_helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,37 @@ def random_string(length=8):
126126
string.ascii_lowercase) for _ in range(length))
127127

128128

129+
def create_roles_with_dependencies(path):
130+
"""
131+
Create a number of roles.
132+
"""
133+
role_names = []
134+
135+
full_path = os.path.join(path, 'role1')
136+
utils.capture_shell("ansigenome init {0} -c system".format(full_path))
137+
meta_path = os.path.join(full_path, 'meta', 'main.yml')
138+
with open(meta_path, "w") as myfile:
139+
myfile.write("---\ndependencies:\n - { role: role2 }\n")
140+
role_names.append(os.path.basename(full_path))
141+
142+
full_path = os.path.join(path, 'role2')
143+
utils.capture_shell("ansigenome init {0} -c system".format(full_path))
144+
meta_path = os.path.join(full_path, 'meta', 'main.yml')
145+
with open(meta_path, "w") as myfile:
146+
myfile.write("---\ndependencies:\n - { role: role3 }\n")
147+
role_names.append(os.path.basename(full_path))
148+
149+
role_names.append(os.path.basename(full_path))
150+
full_path = os.path.join(path, 'role3')
151+
utils.capture_shell("ansigenome init {0} -c system".format(full_path))
152+
meta_path = os.path.join(full_path, 'meta', 'main.yml')
153+
with open(meta_path, "w") as myfile:
154+
myfile.write("---\ndependencies:\n - { role: role4 }\n")
155+
role_names.append(os.path.basename(full_path))
156+
157+
return role_names
158+
159+
129160
def create_roles(path, number=2):
130161
"""
131162
Create a number of roles.

bin/ansigenome

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ from ansigenome.config import Config
1414
from ansigenome.init import Init
1515
from ansigenome.run import Run
1616
from ansigenome.scan import Scan
17+
from ansigenome.reverse_dependencies import ReverseDependencies
18+
19+
1720

1821

1922
def get_action(args):
@@ -123,15 +126,21 @@ def build_option_parser(action):
123126
parser.add_option("-c", "--galaxy-categories",
124127
dest="galaxy_categories",
125128
help="comma separated string of galaxy categories")
129+
elif action == "reverse_dependencies":
130+
parser.set_usage("{0} -r roles [-o OUT_FILE]".format(usage_prefix))
131+
parser.add_option("-r", "--roles",
132+
dest="roles",
133+
help="comma separated string of roles to do analyze")
126134
elif action == "run":
127135
parser.set_usage("{0} ROLES_PATH -m COMMAND".format
128136
(usage_prefix))
129137
parser.add_option("-m", "--command", dest="command",
130138
help="execute this shell command on each role")
131-
if action in ("scan", "gendoc", "genmeta", "export", "run"):
139+
if action in ("scan", "gendoc", "genmeta", "export", "run",
140+
"reverse_dependencies"):
132141
parser.add_option("-l", "--limit", dest="limit",
133142
help="comma separated string of roles to white list")
134-
if action in ("config", "export"):
143+
if action in ("config", "export", "reverse_dependencies"):
135144
parser.add_option("-o", "--out", dest="out_file",
136145
help="output file path")
137146

@@ -188,6 +197,13 @@ def execute_run(args, options, config, parser):
188197
check_roles_path(args, parser)
189198
Run(args, options, config)
190199

200+
def execute_reverse_dependencies(args, options, config, parser):
201+
"""
202+
Execute reverse dependencies.
203+
"""
204+
check_roles_path(args, parser)
205+
Scan(args, options, config, reverse_dependencies=True)
206+
191207

192208
def execute_init(args, options, config, parser):
193209
"""

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
description="A tool to help you gather information and " +
1919
"manage your Ansible roles.",
2020
license="GPLv3",
21-
install_requires=["jinja2", "PyYAML", "setuptools"],
21+
install_requires=["jinja2", "PyYAML", "setuptools", "networkx",
22+
"pyparsing==1.5.7"],
2223
packages=["ansigenome"],
2324
package_data={"ansigenome": ["VERSION", "data/*"]},
2425
scripts=["bin/ansigenome"],
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import unittest
3+
4+
import ansigenome.constants as c
5+
import ansigenome.test_helpers as th
6+
import ansigenome.utils as utils
7+
8+
9+
class TestReverseDependencies(unittest.TestCase):
10+
"""
11+
Integration tests for the genmeta command.
12+
"""
13+
def setUp(self):
14+
self.test_path = os.getenv("ANSIGENOME_TEST_PATH", c.TEST_PATH)
15+
16+
if not os.path.exists(self.test_path):
17+
os.makedirs(self.test_path)
18+
19+
def tearDown(self):
20+
th.rmrf(self.test_path)
21+
22+
def test_reverse_dependencies(self):
23+
th.create_roles_with_dependencies(self.test_path)
24+
25+
# ----------------------------------------------------------------
26+
# test role that doens't exists
27+
# ----------------------------------------------------------------
28+
(out, err) = utils.capture_shell(
29+
"ansigenome reverse_dependencies {0} -r test1".format(
30+
self.test_path))
31+
th.print_out("test role that doens't exists:", out)
32+
self.assertIn("No role named 'test1' was found in the path", out)
33+
34+
# ----------------------------------------------------------------
35+
# test role that exists but has no reverse dependency
36+
# ----------------------------------------------------------------
37+
38+
(out, err) = utils.capture_shell(
39+
"ansigenome reverse_dependencies {0} -r role1".format(
40+
self.test_path))
41+
42+
th.print_out("test role that exists but has no reverse dependency:",
43+
out)
44+
self.assertIn("['role1']", out)
45+
46+
# ----------------------------------------------------------------
47+
# test role that exists and have reverse dependency
48+
# ----------------------------------------------------------------
49+
50+
(out, err) = utils.capture_shell(
51+
"ansigenome reverse_dependencies {0} -r role2".format(
52+
self.test_path))
53+
54+
th.print_out("test role that exists and have reverse dependency:", out)
55+
self.assertIn("'role1'", out)
56+
self.assertIn("'role2'", out)
57+
self.assertNotIn("'role3'", out)
58+
59+
if __name__ == "__main__":
60+
unittest.main()

0 commit comments

Comments
 (0)